From 374b4945d5ef4e215ff390be3491e18fa16012b6 Mon Sep 17 00:00:00 2001 From: Fernando Velasquez Date: Thu, 5 Feb 2026 16:16:09 -0500 Subject: [PATCH 1/3] Added logic to fail test compilation if test SQL or expected SQL are empty --- core/actions/test.ts | 14 ++ core/actions/test_test.ts | 274 +++++++++++++++++++++++++++++++++++++- 2 files changed, 284 insertions(+), 4 deletions(-) diff --git a/core/actions/test.ts b/core/actions/test.ts index b6d69d204..124619967 100644 --- a/core/actions/test.ts +++ b/core/actions/test.ts @@ -192,6 +192,20 @@ export class Test extends ActionBuilder { } this.proto.expectedOutputQuery = testContext.apply(this.contextableQuery); + // Check if the test query and expected output query are non-empty. + if (this.proto.testQuery === null || this.proto.testQuery.trim() === "") { + this.session.compileError( + new Error("Test query is empty."), + this.proto.fileName + ); + } + if (this.proto.expectedOutputQuery === null || this.proto.expectedOutputQuery.trim() === "") { + this.session.compileError( + new Error("Expected query is empty."), + this.proto.fileName + ); + } + return verifyObjectMatchesProto( dataform.Test, this.proto, diff --git a/core/actions/test_test.ts b/core/actions/test_test.ts index 9ddccf712..9e4bab07b 100644 --- a/core/actions/test_test.ts +++ b/core/actions/test_test.ts @@ -1,5 +1,271 @@ -import { suite } from "df/testing"; +// tslint:disable tsr-detect-non-literal-fs-filename +import { expect } from "chai"; +import * as fs from "fs-extra"; +import * as path from "path"; -suite("test", () => { - // This action currently has no unit tests! -}); +import { asPlainObject, suite, test } from "df/testing"; +import { TmpDirFixture } from "df/testing/fixtures"; +import { + coreExecutionRequestFromPath, + runMainInVm, + VALID_WORKFLOW_SETTINGS_YAML +} from "df/testing/run_core"; + +suite("test", ({ afterEach }) => { + const tmpDirFixture = new TmpDirFixture(afterEach); + + test(`test with no inputs`, () => { + const projectDir = tmpDirFixture.createNewTmpDir(); + const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); + const definitionsDir = path.join(projectDir, "definitions"); + const actionsYamlPath = path.join(definitionsDir, "actions.yaml"); + const actionSqlPath = path.join(definitionsDir, "action.sql"); + const actionTestSqlxPath = path.join(definitionsDir, "action_test.sqlx"); + + fs.writeFileSync(workflowSettingsPath, VALID_WORKFLOW_SETTINGS_YAML); + fs.mkdirSync(definitionsDir); + fs.writeFileSync(actionsYamlPath, ` +actions: +- table: + filename: action.sql` + ); + fs.writeFileSync(actionSqlPath, "SELECT 1"); + fs.writeFileSync(actionTestSqlxPath, ` +config { + type: "test", + dataset: "action" +} +SELECT 1`); + + const result = runMainInVm(coreExecutionRequestFromPath(projectDir)); + + expect(result.compile.compiledGraph.graphErrors.compilationErrors).deep.equals([]); + expect(asPlainObject(result.compile.compiledGraph.tests)).deep.equals( + asPlainObject([ + { + // Original test properties + name: "action_test", + testQuery: "SELECT 1", + expectedOutputQuery: "\n\nSELECT 1", + fileName: "definitions/action_test.sqlx", + } + ]) + ); + expect(asPlainObject(result.compile.compiledGraph.tables)).deep.equals( + asPlainObject([ + { + "target": { + "database": "defaultProject", + "name": "action", + "schema": "defaultDataset" + }, + "canonicalTarget": { + "database": "defaultProject", + "name": "action", + "schema": "defaultDataset" + }, + "disabled": false, + "enumType": "TABLE", + "fileName": "definitions/action.sql", + "hermeticity": "NON_HERMETIC", + "query": "SELECT 1", + "type": "table" + } + ])); + }); + + test(`test with multiple_inputs input`, () => { + const projectDir = tmpDirFixture.createNewTmpDir(); + const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); + const definitionsDir = path.join(projectDir, "definitions"); + const actionsYamlPath = path.join(definitionsDir, "actions.yaml"); + const action1SqlxPath = path.join(definitionsDir, "action1.sqlx"); + const action1TestSqlxPath = path.join(definitionsDir, "action1_test.sqlx"); + const action2SqlxPath = path.join(definitionsDir, "action2.sqlx"); + const action2TestSqlxPath = path.join(definitionsDir, "action2_test.sqlx"); + + fs.writeFileSync(workflowSettingsPath, VALID_WORKFLOW_SETTINGS_YAML); + fs.mkdirSync(definitionsDir); + + // Add a declaration + fs.writeFileSync(actionsYamlPath, ` +actions: +- declaration: + name: a_declaration` + ); + + // Add an action with a test, reads from declaration + fs.writeFileSync(action1SqlxPath, ` +config { + type: "table", +} +SELECT a,b,c FROM \${ref("a_declaration")} + `); + fs.writeFileSync(action1TestSqlxPath, ` +config { + type: "test", + dataset: "action1" +} +input "a_declaration" { + SELECT 1 AS a, 2 AS b, 3 AS c, 4 AS d +} +SELECT 1 AS a, 2 AS b, 3 AS c`); + + + // Add an action with a test, reads from previous action + fs.writeFileSync(action2SqlxPath, ` +config { + type: "table", +} +SELECT a,b FROM \${ref("action1")} + `); + fs.writeFileSync(action2TestSqlxPath, ` +config { + type: "test", + dataset: "action2" +} +input "action1" { + SELECT 1 AS a, 2 AS b, 3 AS c +} +SELECT 1 AS a, 2 AS b`); + + const result = runMainInVm(coreExecutionRequestFromPath(projectDir)); + + expect(result.compile.compiledGraph.graphErrors.compilationErrors).deep.equals([]); + expect(asPlainObject(result.compile.compiledGraph.tests)).deep.equals( + asPlainObject([ + { + // Original test properties + name: "action1_test", + testQuery: "\n\nSELECT a,b,c FROM (\n SELECT 1 AS a, 2 AS b, 3 AS c, 4 AS d\n)\n ", + expectedOutputQuery: "\n\n\nSELECT 1 AS a, 2 AS b, 3 AS c", + fileName: "definitions/action1_test.sqlx", + }, + { + // Original test properties + name: "action2_test", + testQuery: "\n\nSELECT a,b FROM (\n SELECT 1 AS a, 2 AS b, 3 AS c\n)\n ", + expectedOutputQuery: "\n\n\nSELECT 1 AS a, 2 AS b", + fileName: "definitions/action2_test.sqlx", + } + ]) + ); + expect(asPlainObject(result.compile.compiledGraph.tables)).deep.equals( + asPlainObject([ + { + "target": { + "database": "defaultProject", + "name": "action1", + "schema": "defaultDataset" + }, + "canonicalTarget": { + "database": "defaultProject", + "name": "action1", + "schema": "defaultDataset" + }, + "dependencyTargets": [ + { + "database": "defaultProject", + "name": "a_declaration", + "schema": "defaultDataset" + } + ], + "disabled": false, + "enumType": "TABLE", + "fileName": "definitions/action1.sqlx", + "hermeticity": "NON_HERMETIC", + "query": "\n\nSELECT a,b,c FROM `defaultProject.defaultDataset.a_declaration`\n ", + "type": "table" + }, + { + "target": { + "database": "defaultProject", + "name": "action2", + "schema": "defaultDataset" + }, + "canonicalTarget": { + "database": "defaultProject", + "name": "action2", + "schema": "defaultDataset" + }, + "dependencyTargets": [ + { + "database": "defaultProject", + "name": "action1", + "schema": "defaultDataset" + } + ], + "disabled": false, + "enumType": "TABLE", + "fileName": "definitions/action2.sqlx", + "hermeticity": "NON_HERMETIC", + "query": "\n\nSELECT a,b FROM `defaultProject.defaultDataset.action1`\n ", + "type": "table" + } + ]) + ); + }); + + test(`test with empty test sql`, () => { + const projectDir = tmpDirFixture.createNewTmpDir(); + const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); + const definitionsDir = path.join(projectDir, "definitions"); + const actionsYamlPath = path.join(definitionsDir, "actions.yaml"); + const action1SqlxPath = path.join(definitionsDir, "action1.sqlx"); + const action1TestSqlxPath = path.join(definitionsDir, "action1_test.sqlx"); + const action2SqlxPath = path.join(definitionsDir, "action2.sqlx"); + const action2TestSqlxPath = path.join(definitionsDir, "action2_test.sqlx"); + + fs.writeFileSync(workflowSettingsPath, VALID_WORKFLOW_SETTINGS_YAML); + fs.mkdirSync(definitionsDir); + + // Add a declaration + fs.writeFileSync(actionsYamlPath, ` +actions: +- declaration: + name: a_declaration` + ); + + // Add an action with a test, reads from declaration + fs.writeFileSync(action1SqlxPath, ` +config { + type: "table", +} +SELECT a,b,c FROM \${ref("a_declaration")} + `); + fs.writeFileSync(action1TestSqlxPath, ` +config { + type: "test", + dataset: "action1" +} +input "a_declaration" { + SELECT 1 AS a, 2 AS b, 3 AS c, 4 AS d +} +`); + + + // Add an action with a test, reads from previous action + fs.writeFileSync(action2SqlxPath, ` +config { + type: "table", +} +SELECT a,b FROM \${ref("action1")} + `); + fs.writeFileSync(action2TestSqlxPath, ` +config { + type: "test", + dataset: "action2" +} +input "action1" { + SELECT 1 AS a, 2 AS b, 3 AS c +} +SELECT 1 AS a, 2 AS b`); + + const result = runMainInVm(coreExecutionRequestFromPath(projectDir)); + + expect(result.compile.compiledGraph.graphErrors.compilationErrors.length).equals(1); + expect(result.compile.compiledGraph.graphErrors.compilationErrors[0].message).contains( + `Expected query is empty.` + ); + }); +}); \ No newline at end of file From b896b845b6e760aabe75c6140b5e7c381d478814 Mon Sep 17 00:00:00 2001 From: Fernando Velasquez Date: Thu, 5 Feb 2026 18:02:47 -0500 Subject: [PATCH 2/3] Addressed PR comments --- core/actions/test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/actions/test.ts b/core/actions/test.ts index 124619967..ea7d531fe 100644 --- a/core/actions/test.ts +++ b/core/actions/test.ts @@ -193,13 +193,13 @@ export class Test extends ActionBuilder { this.proto.expectedOutputQuery = testContext.apply(this.contextableQuery); // Check if the test query and expected output query are non-empty. - if (this.proto.testQuery === null || this.proto.testQuery.trim() === "") { + if (!this.proto.testQuery || this.proto.testQuery.trim() === "") { this.session.compileError( new Error("Test query is empty."), this.proto.fileName ); } - if (this.proto.expectedOutputQuery === null || this.proto.expectedOutputQuery.trim() === "") { + if (!this.proto.expectedOutputQuery || this.proto.expectedOutputQuery.trim() === "") { this.session.compileError( new Error("Expected query is empty."), this.proto.fileName From b65befc21bbc96acb0dc4a261ccb6c094943b9ec Mon Sep 17 00:00:00 2001 From: Fernando Velasquez Date: Fri, 6 Feb 2026 13:03:34 -0500 Subject: [PATCH 3/3] Fix pr comments --- core/actions/test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/actions/test.ts b/core/actions/test.ts index ea7d531fe..811b11a4a 100644 --- a/core/actions/test.ts +++ b/core/actions/test.ts @@ -193,13 +193,13 @@ export class Test extends ActionBuilder { this.proto.expectedOutputQuery = testContext.apply(this.contextableQuery); // Check if the test query and expected output query are non-empty. - if (!this.proto.testQuery || this.proto.testQuery.trim() === "") { + if (!this.proto.testQuery.trim()) { this.session.compileError( new Error("Test query is empty."), this.proto.fileName ); } - if (!this.proto.expectedOutputQuery || this.proto.expectedOutputQuery.trim() === "") { + if (!this.proto.expectedOutputQuery.trim()) { this.session.compileError( new Error("Expected query is empty."), this.proto.fileName