Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions core/actions/test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { verifyObjectMatchesProto, VerifyProtoErrorBehaviour } from "df/common/protos";
import { ActionBuilder, INamedConfig, TableType } from "df/core/actions";
import { IncrementalTable } from "df/core/actions/incremental_table";
import { Table } from "df/core/actions/table";
import { View } from "df/core/actions/view";
import { Table, TableContext } from "df/core/actions/table";
import { View, ViewContext } from "df/core/actions/view";
import { Contextable, IActionContext, ITableContext, Resolvable } from "df/core/contextables";
import { Session } from "df/core/session";
import { targetStringifier } from "df/core/targets";
Expand Down Expand Up @@ -77,7 +77,8 @@ export class Test extends ActionBuilder<dataform.Test> {

/** @hidden We delay contextification until the final compile step, so hold these here for now. */
public contextableInputs = new Map<string, Contextable<IActionContext, string>>();
private contextableQuery: Contextable<IActionContext, string>;
private inputToTargets = new Map<string, dataform.Target>();
private contextableQuery: Contextable<IActionContext, string>;
private datasetToTest: Resolvable;

/**
Expand Down Expand Up @@ -127,13 +128,20 @@ export class Test extends ActionBuilder<dataform.Test> {
* Sets the input query to unit test against.
*/
public input(refName: string | string[], contextableQuery: Contextable<IActionContext, string>) {
const target = resolvableAsTarget(toResolvable(refName));
const inputName = targetStringifier.stringify(target);
this.contextableInputs.set(
targetStringifier.stringify(resolvableAsTarget(toResolvable(refName))),
inputName,
contextableQuery
);
this.inputToTargets.set(inputName, target);
return this;
}

public resolveSchema(resolveSchema: boolean) {
this.proto.resolveSchema = resolveSchema;
}

/**
* Sets the expected output of the query to being tested against.
*/
Expand Down Expand Up @@ -188,7 +196,34 @@ export class Test extends ActionBuilder<dataform.Test> {
} else {
const refReplacingContext = new RefReplacingContext(testContext);
this.proto.testQuery = refReplacingContext.apply(dataset.contextableQuery);

// Set the test query with the fully qualified table references.
if (dataset instanceof Table) {
const tableContext = new TableContext(dataset);
this.proto.query = tableContext.apply(dataset.contextableQuery);
} else {
const viewContext = new ViewContext(dataset);
this.proto.query = viewContext.apply(dataset.contextableQuery);
}
}

// Build the inputs list with the fully qualified input names and the input query to mock each input.
this.contextableInputs.forEach((inputContextable, inputName) => {
const testInput = dataform.Test.TestInput.create();
testInput.query = testContext.apply(inputContextable);
testInput.target = this.applySessionToTarget(
this.inputToTargets.get(inputName),
this.session.projectConfig,
this.getFileName(),
{
validateTarget: true
});
this.proto.inputs.push(testInput);
});

// Add target property/
this.proto.testTarget = dataset.getTarget();
this.proto.resolveSchema = false;
}
this.proto.expectedOutputQuery = testContext.apply(this.contextableQuery);

Expand Down
177 changes: 174 additions & 3 deletions core/actions/test_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,176 @@
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",

// New properties
testTarget: {
database: "defaultProject",
schema: "defaultDataset",
name: "action"
},
query: "SELECT 1",
resolveSchema: false,
}
])
);
});

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",

// New properties
testTarget: {
database: "defaultProject",
schema: "defaultDataset",
name: "action1"
},
inputs: [{
query: "\n SELECT 1 AS a, 2 AS b, 3 AS c, 4 AS d\n",
target: {
database: "defaultProject",
schema: "defaultDataset",
name: "a_declaration"
}
}],
query: "\n\nSELECT a,b,c FROM `defaultProject.defaultDataset.a_declaration`\n ",
resolveSchema: false,
},
{
// 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",

// New properties
testTarget: {
database: "defaultProject",
schema: "defaultDataset",
name: "action2"
},
inputs: [{
query: "\n SELECT 1 AS a, 2 AS b, 3 AS c\n",
target: {
database: "defaultProject",
schema: "defaultDataset",
name: "action1"
}
}],
query: "\n\nSELECT a,b FROM `defaultProject.defaultDataset.action1`\n ",
resolveSchema: false,
}
])
);
});
});
10 changes: 10 additions & 0 deletions protos/core.proto
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,16 @@ message Test {

// Generated.
string file_name = 4;

Target test_target = 5;
repeated TestInput inputs = 6;
string query = 7;
bool resolve_schema = 8;

message TestInput {
Target target = 1;
string query = 2;
}
Comment on lines +268 to +276
Copy link
Contributor

@kolina kolina Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm not ready to accept this proposal:

  • You're adding a lot of new fields and it'll be super confusing for users to understand the relationship between new query and inputs and old test_query and expected_output_query (I myself spend a lot of time to understand it)
  • I'm not sure if all of these fields are really needed, I'll need some explanation on how all of these fields are intended to be used and discuss if it's feasible to have more compact compilation output

}

message Notebook {
Expand Down