diff --git a/cli/index_compile_test.ts b/cli/index_compile_test.ts index db9b30a54..bd0f198ac 100644 --- a/cli/index_compile_test.ts +++ b/cli/index_compile_test.ts @@ -1,13 +1,19 @@ import { expect } from "chai"; -import { execFile } from "child_process"; import * as fs from "fs-extra"; import { dump as dumpYaml, load as loadYaml } from "js-yaml"; import * as path from "path"; -import { cliEntryPointPath, INTEGRATION_TEST_LOCATION, INTEGRATION_TEST_PROJECT } from "df/cli/index_test_base"; +import { + alterWorkflowSettings, + INTEGRATION_TEST_LOCATION, + INTEGRATION_TEST_PROJECT, + runCli, + setupProject, + writeDefinitionFile +} from "df/cli/index_test_base"; import { version } from "df/core/version"; import { dataform } from "df/protos/ts"; -import { corePackageTarPath, getProcessResult, nodePath, npmPath, suite, test } from "df/testing"; +import { suite, test } from "df/testing"; import { TmpDirFixture } from "df/testing/fixtures"; suite("compile command", ({ afterEach }) => { @@ -24,8 +30,7 @@ suite("compile command", ({ afterEach }) => { ); expect( - (await getProcessResult(execFile(nodePath, [cliEntryPointPath, "compile", projectDir]))) - .stderr + (await runCli("compile", [projectDir])).stderr ).contains( "dataformCoreVersion must be specified either in workflow_settings.yaml or via a " + "package.json" @@ -55,8 +60,7 @@ suite("compile command", ({ afterEach }) => { ); expect( - (await getProcessResult(execFile(nodePath, [cliEntryPointPath, "compile", projectDir]))) - .stderr + (await runCli("compile", [projectDir])).stderr ).contains( "Could not find a recent installed version of @dataform/core in the project. Check that " + "either `dataformCoreVersion` is specified in `workflow_settings.yaml`, or " + @@ -84,11 +88,9 @@ suite("compile command", ({ afterEach }) => { // npm needs a writable cache; ~/.npm is read-only in the bazel sandbox. const npmCacheDir = tmpDirFixture.createNewTmpDir(); const stderr = ( - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir], { - env: { ...process.env, NPM_CONFIG_CACHE: npmCacheDir } - }) - ) + await runCli("compile", [projectDir], { + env: { ...process.env, NPM_CONFIG_CACHE: npmCacheDir } + }) ).stderr; expect(stderr).contains("@dataform/core 2.9.0 is not compatible with @dataform/cli"); expect(stderr).contains("matching major.minor"); @@ -111,18 +113,16 @@ suite("compile command", ({ afterEach }) => { dataformCoreVersion: "3.0.50" }) ); - fs.ensureFileSync(path.join(projectDir, "definitions", "example.sqlx")); - fs.writeFileSync( - path.join(projectDir, "definitions", "example.sqlx"), + writeDefinitionFile( + projectDir, + "example.sqlx", `config { type: "table" }\nSELECT 1 AS id` ); const npmCacheDir = tmpDirFixture.createNewTmpDir(); - const result = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir, "--json"], { - env: { ...process.env, NPM_CONFIG_CACHE: npmCacheDir } - }) - ); + const result = await runCli("compile", [projectDir, "--json"], { + env: { ...process.env, NPM_CONFIG_CACHE: npmCacheDir } + }); expect(result.exitCode, `compile failed: ${result.stderr}`).equals(0); const compiled = JSON.parse(result.stdout); @@ -150,8 +150,7 @@ suite("compile command", ({ afterEach }) => { } expect( - (await getProcessResult(execFile(nodePath, [cliEntryPointPath, "compile", projectDir]))) - .stderr + (await runCli("compile", [projectDir])).stderr ).contains(`${npmFile}' unexpected; remove it and try again`); }); }); @@ -162,53 +161,20 @@ suite("disable-assertions flag (compilation)", ({ afterEach, beforeEach }) => { let projectDir: string; async function setupTestProject(): Promise { - const npmCacheDir = tmpDirFixture.createNewTmpDir(); - const packageJsonPath = path.join(projectDir, "package.json"); - - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); + await setupProject(tmpDirFixture, projectDir); - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - - fs.writeFileSync( - packageJsonPath, - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` - ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", - projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) - ); - - const assertionFilePath = path.join(projectDir, "definitions", "test_assertion.sqlx"); - fs.ensureFileSync(assertionFilePath); - fs.writeFileSync( - assertionFilePath, + writeDefinitionFile( + projectDir, + "test_assertion.sqlx", ` config { type: "assertion" } SELECT 1 WHERE FALSE ` ); - const tableFilePath = path.join(projectDir, "definitions", "example_table.sqlx"); - fs.ensureFileSync(tableFilePath); - fs.writeFileSync( - tableFilePath, + writeDefinitionFile( + projectDir, + "example_table.sqlx", ` config { type: "table", @@ -221,15 +187,6 @@ SELECT 1 as id ); } - async function setUpWorkflowSettings(disableAssertions: boolean): Promise { - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - workflowSettings.disableAssertions = disableAssertions; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - } - beforeEach("setup test project", async () => { projectDir = tmpDirFixture.createNewTmpDir(); await setupTestProject(); @@ -333,101 +290,71 @@ SELECT 1 as id }; test("with --disable-assertions flag", async () => { - await setUpWorkflowSettings(false); - - const compileResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "compile", - projectDir, - "--json", - "--disable-assertions" - ]) - ); + alterWorkflowSettings(projectDir, { disableAssertions: false }); + + const compileResult = await runCli("compile", [ + projectDir, + "--json", + "--disable-assertions" + ]); expect(compileResult.exitCode).equals(0); expect(JSON.parse(compileResult.stdout)).deep.equals(expectedCompileResult); }); test("with disableAssertions set in workflow_settings.yaml", async () => { - await setUpWorkflowSettings(true); + alterWorkflowSettings(projectDir, { disableAssertions: true }); - const compileResult = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir, "--json"]) - ); + const compileResult = await runCli("compile", [projectDir, "--json"]); expect(compileResult.exitCode).equals(0); expect(JSON.parse(compileResult.stdout)).deep.equals(expectedCompileResult); }); }); -suite("compile node selection", ({ afterEach }) => { +suite("compile node selection", ({ afterEach, beforeEach }) => { const tmpDirFixture = new TmpDirFixture(afterEach); - // Builds a project with three tables: upstream -> midstream -> downstream. - async function setupSelectionProject(): Promise { - const projectDir = tmpDirFixture.createNewTmpDir(); - const npmCacheDir = tmpDirFixture.createNewTmpDir(); + const tableNames = (stdout: string): string[] => + JSON.parse(stdout).tables.map((table: any) => table.target.name).sort(); - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); + let projectDir: string; - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); + beforeEach("setup test project", async () => { + // Builds a project with three tables: upstream -> midstream -> downstream. + projectDir = tmpDirFixture.createNewTmpDir(); + await setupProject(tmpDirFixture, projectDir); - fs.writeFileSync( - path.join(projectDir, "package.json"), - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` + writeDefinitionFile( + projectDir, + "upstream.sqlx", + `config { type: "table", tags: ["daily"] }\nSELECT 1 AS id` ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", - projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) + writeDefinitionFile( + projectDir, + "midstream.sqlx", + `config { type: "table" }\nSELECT * FROM \${ref("upstream")}` ); - - const def = (name: string, contents: string) => { - const filePath = path.join(projectDir, "definitions", `${name}.sqlx`); - fs.ensureFileSync(filePath); - fs.writeFileSync(filePath, contents); - }; - def("upstream", `config { type: "table", tags: ["daily"] }\nSELECT 1 AS id`); - def("midstream", `config { type: "table" }\nSELECT * FROM \${ref("upstream")}`); - def("downstream", `config { type: "table" }\nSELECT * FROM \${ref("midstream")}`); - - return projectDir; - } - - const tableNames = (stdout: string): string[] => - JSON.parse(stdout).tables.map((table: any) => table.target.name).sort(); + writeDefinitionFile( + projectDir, + "downstream.sqlx", + `config { type: "table" }\nSELECT * FROM \${ref("midstream")}` + ); + }); test("no selector emits the entire graph", async () => { - const projectDir = await setupSelectionProject(); - const result = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir, "--json"]) - ); + const result = await runCli("compile", [projectDir, "--json"]); expect(result.exitCode, result.stderr).equals(0); expect(tableNames(result.stdout)).deep.equals(["downstream", "midstream", "upstream"]); }); test("--output-actions filters output to the selected action", async () => { - const projectDir = await setupSelectionProject(); - const result = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir, "--output-actions", "midstream", "--json"]) - ); + const result = await runCli("compile", [ + projectDir, + "--output-actions", + "midstream", + "--json" + ]); expect(result.exitCode, result.stderr).equals(0); expect(tableNames(result.stdout)).deep.equals(["midstream"]); // `targets` is printed alongside the actions, so it must be pruned too. @@ -437,62 +364,57 @@ suite("compile node selection", ({ afterEach }) => { }); test("--output-actions --output-include-deps pulls in upstream dependencies", async () => { - const projectDir = await setupSelectionProject(); - const result = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "compile", - projectDir, - "--output-actions", - "midstream", - "--output-include-deps", - "--json" - ]) - ); + const result = await runCli("compile", [ + projectDir, + "--output-actions", + "midstream", + "--output-include-deps", + "--json" + ]); expect(result.exitCode, result.stderr).equals(0); expect(tableNames(result.stdout)).deep.equals(["midstream", "upstream"]); }); test("--output-actions --output-include-dependents pulls in downstream dependents", async () => { - const projectDir = await setupSelectionProject(); - const result = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "compile", - projectDir, - "--output-actions", - "midstream", - "--output-include-dependents", - "--json" - ]) - ); + const result = await runCli("compile", [ + projectDir, + "--output-actions", + "midstream", + "--output-include-dependents", + "--json" + ]); expect(result.exitCode, result.stderr).equals(0); expect(tableNames(result.stdout)).deep.equals(["downstream", "midstream"]); }); test("--output-tags filters output to actions carrying the tag", async () => { - const projectDir = await setupSelectionProject(); - const result = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir, "--output-tags", "daily", "--json"]) - ); + const result = await runCli("compile", [ + projectDir, + "--output-tags", + "daily", + "--json" + ]); expect(result.exitCode, result.stderr).equals(0); expect(tableNames(result.stdout)).deep.equals(["upstream"]); }); test("selector matching nothing emits an empty graph and exits zero", async () => { - const projectDir = await setupSelectionProject(); - const result = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir, "--output-actions", "nope", "--json"]) - ); + const result = await runCli("compile", [ + projectDir, + "--output-actions", + "nope", + "--json" + ]); expect(result.exitCode, result.stderr).equals(0); expect(tableNames(result.stdout)).deep.equals([]); }); test("--output-include-deps without a selector is rejected", async () => { - const projectDir = await setupSelectionProject(); - const result = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir, "--output-include-deps", "--json"]) - ); + const result = await runCli("compile", [ + projectDir, + "--output-include-deps", + "--json" + ]); expect(result.exitCode).not.equals(0); expect(result.stderr).contains("--output-include-deps"); }); @@ -503,46 +425,16 @@ suite("extension config", ({ afterEach }) => { test("compile succeeds with extension set in workflow_settings.yaml", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); - const npmCacheDir = tmpDirFixture.createNewTmpDir(); - - fs.writeFileSync( - path.join(projectDir, "workflow_settings.yaml"), - dumpYaml({ - defaultProject: INTEGRATION_TEST_PROJECT, - defaultLocation: INTEGRATION_TEST_LOCATION, - defaultDataset: "dataform", - defaultAssertionDataset: "dataform_assertions", - extension: { - name: "test-extension", - compilationMode: "PROLOGUE", - }, - }) - ); - fs.mkdirSync(path.join(projectDir, "definitions")); - fs.mkdirSync(path.join(projectDir, "includes")); - - fs.writeFileSync( - path.join(projectDir, "package.json"), - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` - ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", - projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) - ); + await setupProject(tmpDirFixture, projectDir, { + defaultDataset: "dataform", + defaultAssertionDataset: "dataform_assertions", + extension: { + name: "test-extension", + compilationMode: dataform.ExtensionCompilationMode.PROLOGUE + } + }); - const compileResult = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir]) - ); + const compileResult = await runCli("compile", [projectDir]); expect(compileResult.exitCode).equals(0); expect(compileResult.stdout).contains("Compiled 0 action(s)."); diff --git a/cli/index_help_test.ts b/cli/index_help_test.ts index 258e16727..9122db956 100644 --- a/cli/index_help_test.ts +++ b/cli/index_help_test.ts @@ -1,12 +1,11 @@ import { expect } from "chai"; -import { execFile } from "child_process"; -import { cliEntryPointPath } from "df/cli/index_test_base"; -import { getProcessResult, nodePath, suite, test } from "df/testing"; +import { runCli } from "df/cli/index_test_base"; +import { suite, test } from "df/testing"; suite("help command", () => { test("shows global help with the help command", async () => { - const result = await getProcessResult(execFile(nodePath, [cliEntryPointPath, "help"])); + const result = await runCli("help"); expect(result.exitCode).equals(0); const output = result.stdout; expect(output).to.include("dataform [command]"); @@ -20,7 +19,7 @@ suite("help command", () => { }); test("shows help for 'init' command", async () => { - const result = await getProcessResult(execFile(nodePath, [cliEntryPointPath, "help", "init"])); + const result = await runCli("help", ["init"]); expect(result.exitCode).equals(0); const output = result.stdout; expect(output).to.include("Create a new dataform project."); @@ -29,7 +28,7 @@ suite("help command", () => { }); test("shows help for 'install' command", async () => { - const result = await getProcessResult(execFile(nodePath, [cliEntryPointPath, "help", "install"])); + const result = await runCli("help", ["install"]); expect(result.exitCode).equals(0); const output = result.stdout; expect(output).to.include("Install a project's NPM dependencies."); @@ -37,7 +36,7 @@ suite("help command", () => { }); test("shows help for 'init-creds' command", async () => { - const result = await getProcessResult(execFile(nodePath, [cliEntryPointPath, "help", "init-creds"])); + const result = await runCli("help", ["init-creds"]); expect(result.exitCode).equals(0); const output = result.stdout; expect(output).to.include("Create a .df-credentials.json file for Dataform to use when accessing BigQuery."); @@ -47,7 +46,7 @@ suite("help command", () => { }); test("shows help for 'compile' command", async () => { - const result = await getProcessResult(execFile(nodePath, [cliEntryPointPath, "help", "compile"])); + const result = await runCli("help", ["compile"]); expect(result.exitCode).equals(0); const output = result.stdout; expect(output).to.include("Compile the dataform project."); @@ -57,7 +56,7 @@ suite("help command", () => { }); test("shows help for 'test' command", async () => { - const result = await getProcessResult(execFile(nodePath, [cliEntryPointPath, "help", "test"])); + const result = await runCli("help", ["test"]); expect(result.exitCode).equals(0); const output = result.stdout; expect(output).to.include("Run the dataform project's unit tests."); @@ -69,7 +68,7 @@ suite("help command", () => { }); test("shows help for 'run' command", async () => { - const result = await getProcessResult(execFile(nodePath, [cliEntryPointPath, "help", "run"])); + const result = await runCli("help", ["run"]); expect(result.exitCode).equals(0); const output = result.stdout; expect(output).to.include("Run the dataform project."); @@ -84,8 +83,8 @@ suite("help command", () => { expect(output).to.include("--job-labels"); }); - test("shows help for 'format' command", async () => { - const result = await getProcessResult(execFile(nodePath, [cliEntryPointPath, "help", "format"])); + test("shows help for 'format' command", async () => { + const result = await runCli("help", ["format"]); expect(result.exitCode).equals(0); const output = result.stdout; expect(output).to.include("Format the dataform project's files."); diff --git a/cli/index_init_test.ts b/cli/index_init_test.ts index 5ca0006aa..1868b89ae 100644 --- a/cli/index_init_test.ts +++ b/cli/index_init_test.ts @@ -1,10 +1,9 @@ import { assert, expect } from "chai"; -import { execFile } from "child_process"; import * as fs from "fs-extra"; import { load as loadYaml } from "js-yaml"; import * as path from "path"; -import { cliEntryPointPath } from "df/cli/index_test_base"; +import { runCli } from "df/cli/index_test_base"; import { ICEBERG_BUCKET_NAME_HINT, ICEBERG_BUCKET_NAME_PROMPT_QUESTION, @@ -20,7 +19,7 @@ import { } from "df/cli/util"; import { version } from "df/core/version"; import { dataform } from "df/protos/ts"; -import { getProcessResult, nodePath, suite, test } from "df/testing"; +import { suite, test } from "df/testing"; import { TmpDirFixture } from "df/testing/fixtures"; suite("init command", ({ afterEach }) => { @@ -29,15 +28,11 @@ suite("init command", ({ afterEach }) => { test("workflow_settings.yaml generated from init", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); - await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "init", - projectDir, - "--default-database=dataform-database", - "--default-location=us-central1" - ]) - ); + await runCli("init", [ + projectDir, + "--default-database=dataform-database", + "--default-location=us-central1" + ]); expect(fs.readFileSync(path.join(projectDir, "workflow_settings.yaml"), "utf8")).to .equal(`dataformCoreVersion: ${version} @@ -58,18 +53,18 @@ defaultAssertionDataset: dataform_assertions [ICEBERG_CONNECTION_QUESTION]: "my.default.connection", }; - const result = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "init", + const result = await runCli( + "init", + [ projectDir, "dataform-iceberg-test", "us-central1", "--iceberg" - ], { + ], + { // Inject test inputs via environment variable env: { ...process.env, DATAFORM_CLI_TEST_INPUTS: JSON.stringify(testInputs) } - }) + } ); expect(result.exitCode).equals(0); @@ -100,18 +95,18 @@ defaultAssertionDataset: dataform_assertions [ICEBERG_CONNECTION_QUESTION]: "my.default.connection", }; - const result = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "init", + const result = await runCli( + "init", + [ projectDir, "dataform-iceberg-partial", "us-east1", "--iceberg" - ], { + ], + { // Inject test inputs via environment variable env: { ...process.env, DATAFORM_CLI_TEST_INPUTS: JSON.stringify(testInputs) } - }) + } ); expect(result.exitCode).equals(0); @@ -146,18 +141,18 @@ defaultAssertionDataset: dataform_assertions [ICEBERG_CONNECTION_QUESTION]: "my.default.connection", }; - const result = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "init", + const result = await runCli( + "init", + [ projectDir, "dataform-iceberg-partial", "us-east1", "--iceberg" - ], { + ], + { // Inject test inputs via environment variable env: { ...process.env, DATAFORM_CLI_TEST_INPUTS: JSON.stringify(testInputs) } - }) + } ); expect(result.exitCode).equals(0); @@ -192,18 +187,18 @@ defaultAssertionDataset: dataform_assertions [ICEBERG_CONNECTION_QUESTION]: "my.default.connection", }; - const result = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "init", + const result = await runCli( + "init", + [ projectDir, "dataform-iceberg-partial", "us-east1", "--iceberg" - ], { + ], + { // Inject test inputs via environment variable env: { ...process.env, DATAFORM_CLI_TEST_INPUTS: JSON.stringify(testInputs) } - }) + } ); expect(result.exitCode).equals(0); @@ -238,18 +233,18 @@ defaultAssertionDataset: dataform_assertions [ICEBERG_CONNECTION_QUESTION]: "", // Empty input }; - const result = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "init", + const result = await runCli( + "init", + [ projectDir, "dataform-iceberg-partial", "us-east1", "--iceberg" - ], { + ], + { // Inject test inputs via environment variable env: { ...process.env, DATAFORM_CLI_TEST_INPUTS: JSON.stringify(testInputs) } - }) + } ); expect(result.exitCode).equals(0); @@ -282,9 +277,7 @@ suite("init-creds command", ({ afterEach }) => { test("init-creds fails for directory without dataform config", async () => { const emptyDir = tmpDirFixture.createNewTmpDir(); - const result = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init-creds", emptyDir]) - ); + const result = await runCli("init-creds", [emptyDir]); expect(result.exitCode).to.not.equal(0); expect(result.stderr).to.include( `${emptyDir} does not appear to be a dataform directory (missing workflow_settings.yaml file).` diff --git a/cli/index_project_test.ts b/cli/index_project_test.ts index 50fc71098..e44c350c2 100644 --- a/cli/index_project_test.ts +++ b/cli/index_project_test.ts @@ -1,13 +1,13 @@ import { expect } from "chai"; -import { execFile } from "child_process"; import * as fs from "fs-extra"; -import { dump as dumpYaml, load as loadYaml } from "js-yaml"; import * as path from "path"; -import { cliEntryPointPath, INTEGRATION_TEST_LOCATION, INTEGRATION_TEST_PROJECT } from "df/cli/index_test_base"; -import { version } from "df/core/version"; -import { dataform } from "df/protos/ts"; -import { corePackageTarPath, getProcessResult, nodePath, npmPath, suite, test } from "df/testing"; +import { + runCli, + setupProject, + writeDefinitionFile +} from "df/cli/index_test_base"; +import { suite, test } from "df/testing"; import { TmpDirFixture } from "df/testing/fixtures"; suite("project ops", ({ afterEach }) => { @@ -17,19 +17,14 @@ suite("project ops", ({ afterEach }) => { test("install throws an error when dataformCoreVersion in workflow_settings.yaml", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); - await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "init", - projectDir, - "--default-database=dataform-database", - "--default-location=us-central1" - ]) - ); + await runCli("init", [ + projectDir, + "--default-database=dataform-database", + "--default-location=us-central1" + ]); expect( - (await getProcessResult(execFile(nodePath, [cliEntryPointPath, "install", projectDir]))) - .stderr + (await runCli("install", [projectDir])).stderr ).contains( "No installation is needed when using workflow_settings.yaml, as packages are installed at " + "runtime." @@ -40,45 +35,12 @@ suite("project ops", ({ afterEach }) => { suite("format command", () => { test("test for format command", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); - const npmCacheDir = tmpDirFixture.createNewTmpDir(); - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const packageJsonPath = path.join(projectDir, "package.json"); - - // Initialize a project using the CLI, don't install packages. - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); - - // Install packages manually to get around bazel read-only sandbox issues. - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - fs.writeFileSync( - packageJsonPath, - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` - ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", - projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) - ); + await setupProject(tmpDirFixture, projectDir); // Create a correctly formatted file - const formattedFilePath = path.join(projectDir, "definitions", "formatted.sqlx"); - fs.ensureFileSync(formattedFilePath); - fs.writeFileSync( - formattedFilePath, + writeDefinitionFile( + projectDir, + "formatted.sqlx", ` config { type: "table" @@ -90,10 +52,9 @@ SELECT ); // Create a file that needs formatting (extra spaces, inconsistent indentation) - const unformattedFilePath = path.join(projectDir, "definitions", "unformatted.sqlx"); - fs.ensureFileSync(unformattedFilePath); - fs.writeFileSync( - unformattedFilePath, + writeDefinitionFile( + projectDir, + "unformatted.sqlx", ` config { type: "table" } SELECT 1 as test @@ -101,9 +62,7 @@ SELECT 1 as test ); // Test with --check flag on a project with files needing formatting - const beforeFormatCheckResult = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "format", projectDir, "--check"]) - ); + const beforeFormatCheckResult = await runCli("format", [projectDir, "--check"]); // Should exit with code 1 when files need formatting expect(beforeFormatCheckResult.exitCode).equals(1); @@ -111,15 +70,11 @@ SELECT 1 as test expect(beforeFormatCheckResult.stderr).contains("unformatted.sqlx"); // Format the files (without check flag) - const formatCheckResult = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "format", projectDir]) - ); + const formatCheckResult = await runCli("format", [projectDir]); expect(formatCheckResult.exitCode).equals(0); // Test with --check flag after formatting - const afterFormatCheckResult = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "format", projectDir, "--check"]) - ); + const afterFormatCheckResult = await runCli("format", [projectDir, "--check"]); // Should exit with code 0 when all files are properly formatted expect(afterFormatCheckResult.exitCode).equals(0); @@ -128,59 +83,25 @@ SELECT 1 as test test("test for format command ignore js files", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); - const npmCacheDir = tmpDirFixture.createNewTmpDir(); - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const packageJsonPath = path.join(projectDir, "package.json"); - - // Initialize a project using the CLI, don't install packages. - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); - - // Install packages manually to get around bazel read-only sandbox issues. - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - fs.writeFileSync( - packageJsonPath, - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` - ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", - projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) - ); + await setupProject(tmpDirFixture, projectDir); // Create files that need formatting and ensure that the js file is not modified - const unformattedFilePath = path.join(projectDir, "definitions", "unformatted.sqlx"); - fs.ensureFileSync(unformattedFilePath); - fs.writeFileSync( - unformattedFilePath, + writeDefinitionFile( + projectDir, + "unformatted.sqlx", ` config { type: "table" } SELECT 1 as test ` ); - const jsContents = ` function myCoolFn() { return true; } modules.exports = { myCoolFn, } -` +`; const unformattedJsFilePath = path.join(projectDir, "includes", "someMod.js"); fs.ensureFileSync(unformattedJsFilePath); fs.writeFileSync( @@ -189,16 +110,14 @@ modules.exports = { ); // Run formatter - const formatCmdRun = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "format", "--ignore-js-files", projectDir]) - ); + const formatCmdRun = await runCli("format", [projectDir, "--ignore-js-files"]); expect(formatCmdRun.exitCode).equals(0); // Ensure the js file didn't change const bufFromFile = fs.readFileSync(unformattedJsFilePath); - const bufFromContents = Buffer.from(jsContents, 'utf-8'); - expect(bufFromContents.equals(bufFromFile)).equals(true) + const bufFromContents = Buffer.from(jsContents, "utf-8"); + expect(bufFromContents.equals(bufFromFile)).equals(true); }); }); }); diff --git a/cli/index_run_e2e_test.ts b/cli/index_run_e2e_test.ts index 452796d6b..0cbb92ab7 100644 --- a/cli/index_run_e2e_test.ts +++ b/cli/index_run_e2e_test.ts @@ -1,19 +1,21 @@ import { expect } from "chai"; -import { execFile } from "child_process"; import * as fs from "fs-extra"; import { dump as dumpYaml, load as loadYaml } from "js-yaml"; import * as path from "path"; import { - cliEntryPointPath, + alterWorkflowSettings, CREDENTIALS_PATH, INTEGRATION_TEST_LOCATION, INTEGRATION_TEST_PROJECT, - INTEGRATION_TEST_RESERVATION + INTEGRATION_TEST_RESERVATION, + runCli, + setupProject, + writeDefinitionFile } from "df/cli/index_test_base"; import { version } from "df/core/version"; import { dataform } from "df/protos/ts"; -import { corePackageTarPath, getProcessResult, nodePath, npmPath, suite, test } from "df/testing"; +import { suite, test } from "df/testing"; import { TmpDirFixture } from "df/testing/fixtures"; suite("run e2e", ({ afterEach }) => { @@ -21,45 +23,12 @@ suite("run e2e", ({ afterEach }) => { test("golden path with package.json", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); - const npmCacheDir = tmpDirFixture.createNewTmpDir(); - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const packageJsonPath = path.join(projectDir, "package.json"); - - // Initialize a project using the CLI, don't install packages. - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); - - // Install packages manually to get around bazel read-only sandbox issues. - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - fs.writeFileSync( - packageJsonPath, - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` - ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", - projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) - ); + await setupProject(tmpDirFixture, projectDir); // Write a simple file to the project. - const filePath = path.join(projectDir, "definitions", "example.sqlx"); - fs.ensureFileSync(filePath); - fs.writeFileSync( - filePath, + writeDefinitionFile( + projectDir, + "example.sqlx", ` config { type: "table", tags: ["someTag"] } select 1 as \${dataform.projectConfig.vars.testVar2} @@ -67,15 +36,14 @@ select 1 as \${dataform.projectConfig.vars.testVar2} ); // Compile the project using the CLI. - const compileResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "compile", + const compileResult = await runCli( + "compile", + [ projectDir, "--json", "--vars=testVar1=testValue1,testVar2=testValue2", "--schema-suffix=test_schema_suffix" - ]) + ] ); expect(compileResult.exitCode).equals(0); @@ -127,10 +95,9 @@ select 1 as \${dataform.projectConfig.vars.testVar2} }); // Dry run the project. - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + const runResult = await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, @@ -140,7 +107,7 @@ select 1 as \${dataform.projectConfig.vars.testVar2} "--default-location=europe", "--tags=someTag,someOtherTag", "--actions=example,someOtherAction" - ]) + ] ); if (runResult.exitCode !== 0 || runResult.stdout.trim().length === 0) { @@ -193,56 +160,24 @@ select 1 as \${dataform.projectConfig.vars.testVar2} }); suite("disable-assertions flag (run)", ({ beforeEach }) => { - const projectDir = tmpDirFixture.createNewTmpDir(); - - async function setupTestProject(): Promise { - const npmCacheDir = tmpDirFixture.createNewTmpDir(); - const packageJsonPath = path.join(projectDir, "package.json"); + let projectDir: string; - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); - - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - - fs.writeFileSync( - packageJsonPath, - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` - ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", - projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) - ); + beforeEach("setup test project", async () => { + projectDir = tmpDirFixture.createNewTmpDir(); + await setupProject(tmpDirFixture, projectDir); - const assertionFilePath = path.join(projectDir, "definitions", "test_assertion.sqlx"); - fs.ensureFileSync(assertionFilePath); - fs.writeFileSync( - assertionFilePath, + writeDefinitionFile( + projectDir, + "test_assertion.sqlx", ` config { type: "assertion" } SELECT 1 WHERE FALSE ` ); - const tableFilePath = path.join(projectDir, "definitions", "example_table.sqlx"); - fs.ensureFileSync(tableFilePath); - fs.writeFileSync( - tableFilePath, + writeDefinitionFile( + projectDir, + "example_table.sqlx", ` config { type: "table", @@ -253,19 +188,7 @@ config { SELECT 1 as id ` ); - } - - async function setUpWorkflowSettings(disableAssertions: boolean): Promise { - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - workflowSettings.disableAssertions = disableAssertions; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - } - - beforeEach("setup test project", async () => await setupTestProject()); + }); const expectedRunResult = { actions: [ @@ -316,12 +239,11 @@ SELECT 1 as id }; test("with --disable-assertions flag", async () => { - await setUpWorkflowSettings(false); + alterWorkflowSettings(projectDir, { disableAssertions: false }); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + const runResult = await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, @@ -329,7 +251,7 @@ SELECT 1 as id "--json", "--disable-assertions", "--actions=test_assertion,example_table" - ]) + ] ); if (runResult.exitCode !== 0 || runResult.stdout.trim().length === 0) { @@ -341,19 +263,18 @@ SELECT 1 as id }); test("with disableAssertions set in workflow_settings.yaml", async () => { - await setUpWorkflowSettings(true); + alterWorkflowSettings(projectDir, { disableAssertions: true }); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + const runResult = await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, "--dry-run", "--json", "--actions=test_assertion,example_table" - ]) + ] ); if (runResult.exitCode !== 0 || runResult.stdout.trim().length === 0) { @@ -365,12 +286,11 @@ SELECT 1 as id }); test("with --job-labels flag", async () => { - await setUpWorkflowSettings(false); + alterWorkflowSettings(projectDir, { disableAssertions: false }); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + const runResult = await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, @@ -379,7 +299,7 @@ SELECT 1 as id "--disable-assertions", "--actions=test_assertion,example_table", "--job-labels=env=testing,team=dataform" - ]) + ] ); if (runResult.exitCode !== 0 || runResult.stdout.trim().length === 0) { @@ -393,47 +313,15 @@ SELECT 1 as id suite("--default-reservation flag", ({ beforeEach }) => { - const projectDir = tmpDirFixture.createNewTmpDir(); + let projectDir: string; beforeEach("setup test project", async () => { - const npmCacheDir = tmpDirFixture.createNewTmpDir(); - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const packageJsonPath = path.join(projectDir, "package.json"); - - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); + projectDir = tmpDirFixture.createNewTmpDir(); + await setupProject(tmpDirFixture, projectDir); - // Remove dataformCoreVersion so we can use the local package. - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - - fs.writeFileSync( - packageJsonPath, - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` - ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", - projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) - ); - - const tableFilePath = path.join(projectDir, "definitions", "example_table.sqlx"); - fs.ensureFileSync(tableFilePath); - fs.writeFileSync( - tableFilePath, + writeDefinitionFile( + projectDir, + "example_table.sqlx", ` config { type: "table" } SELECT 1 as id @@ -442,14 +330,13 @@ SELECT 1 as id }); test("--default-reservation flag is applied to projectConfig in compile output", async () => { - const compileResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "compile", + const compileResult = await runCli( + "compile", + [ projectDir, "--json", `--default-reservation=${INTEGRATION_TEST_RESERVATION}` - ]) + ] ); expect(compileResult.exitCode).equals(0); @@ -465,10 +352,9 @@ SELECT 1 as id }); test("--default-reservation flag is applied to projectConfig in run (dry-run) output", async () => { - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + const runResult = await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, @@ -476,7 +362,7 @@ SELECT 1 as id "--json", `--default-reservation=${INTEGRATION_TEST_RESERVATION}`, "--actions=example_table" - ]) + ] ); expect(runResult.exitCode).equals(0); @@ -492,205 +378,99 @@ SELECT 1 as id }); }); - test("golden with successful unit test", async () => { - const projectDir = tmpDirFixture.createNewTmpDir(); - const npmCacheDir = tmpDirFixture.createNewTmpDir(); - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const packageJsonPath = path.join(projectDir, "package.json"); - - // Initialize a project using the CLI, don't install packages. - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); - - // Install packages manually to get around bazel read-only sandbox issues. - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - fs.writeFileSync( - packageJsonPath, - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` - ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", - projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) - ); + suite("unit tests", ({ beforeEach }) => { + let projectDir: string; - // Write a simple file to the project. - const filePath = path.join(projectDir, "definitions", "example.sqlx"); - fs.ensureFileSync(filePath); - fs.writeFileSync( - filePath, - ` + beforeEach("setup test project", async () => { + projectDir = tmpDirFixture.createNewTmpDir(); + await setupProject(tmpDirFixture, projectDir); + // Write a simple file to the project. + writeDefinitionFile( + projectDir, + "example.sqlx", + ` config { type: "table" } select 1 ` - ); - // Write a simple test to the project. - const unitTestPath = path.join(projectDir, "definitions", "example_test.sqlx"); - fs.ensureFileSync(unitTestPath); - fs.writeFileSync( - unitTestPath, + ); + }); + + test("golden with successful unit test", async () => { + // Write a simple passing test to the project. + writeDefinitionFile( + projectDir, + "example_test.sqlx", ` config { type: "test", dataset: "example" } select 1 ` - ); + ); - // Run tests using the CLI. - const testResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, + // Run tests using the CLI. + const testResult = await runCli( "test", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--json", - ]) - ); + [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--json" + ] + ); - expect(testResult.exitCode).equals(0); + expect(testResult.exitCode).equals(0); - expect(JSON.parse(testResult.stdout)).deep.equals([ { - "name": "example_test", - "successful": true, - }]); + expect(JSON.parse(testResult.stdout)).deep.equals([ { + "name": "example_test", + "successful": true, + }]); }); - test("golden with failed unit test", async () => { - const projectDir = tmpDirFixture.createNewTmpDir(); - const npmCacheDir = tmpDirFixture.createNewTmpDir(); - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const packageJsonPath = path.join(projectDir, "package.json"); - - // Initialize a project using the CLI, don't install packages. - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); - - // Install packages manually to get around bazel read-only sandbox issues. - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - fs.writeFileSync( - packageJsonPath, - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` - ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", + test("golden with failed unit test", async () => { + // Write a simple failing test to the project. + writeDefinitionFile( projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) - ); - - // Write a simple file to the project. - const filePath = path.join(projectDir, "definitions", "example.sqlx"); - fs.ensureFileSync(filePath); - fs.writeFileSync( - filePath, - ` -config { type: "table" } -select 1 -` - ); - // Write a simple test to the project. - const unitTestPath = path.join(projectDir, "definitions", "example_test.sqlx"); - fs.ensureFileSync(unitTestPath); - fs.writeFileSync( - unitTestPath, - ` + "example_test.sqlx", + ` config { type: "test", dataset: "example" } select 2 ` - ); + ); - // Run tests using the CLI. - const testResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, + // Run tests using the CLI. + const testResult = await runCli( "test", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--json", - ]) - ); + [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--json" + ] + ); - expect(testResult.exitCode).equals(1); + expect(testResult.exitCode).equals(1); + + expect(JSON.parse(testResult.stdout)).deep.equals([{ + "name": "example_test", + "successful": false, + messages: [ + "For row 0 and column \"f0_\": expected \"2\", but saw \"1\"." + ] + }]); + }); - expect(JSON.parse(testResult.stdout)).deep.equals([{ - "name": "example_test", - "successful": false, - messages: [ - "For row 0 and column \"f0_\": expected \"2\", but saw \"1\"." - ] - }]); }); suite("onSchemaChange", ({ beforeEach }) => { - const projectDir = tmpDirFixture.createNewTmpDir(); + let projectDir: string; const uniqueDataset = `dataform_e2e_osc_${Math.random().toString(36).substring(7)}`; beforeEach("setup test project", async () => { - const npmCacheDir = tmpDirFixture.createNewTmpDir(); - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const packageJsonPath = path.join(projectDir, "package.json"); - - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); - - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); - delete workflowSettings.dataformCoreVersion; - workflowSettings.defaultDataset = uniqueDataset; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - - fs.writeFileSync( - packageJsonPath, - `{ - "dependencies":{ - "@dataform/core": "${version}" - } -}` - ); - await getProcessResult( - execFile(npmPath, [ - "install", - "--prefix", - projectDir, - "--cache", - npmCacheDir, - corePackageTarPath - ]) - ); + projectDir = tmpDirFixture.createNewTmpDir(); + await setupProject(tmpDirFixture, projectDir, { defaultDataset: uniqueDataset }); - fs.ensureFileSync(path.join(projectDir, "definitions", "setup_table.sqlx")); - fs.writeFileSync( - path.join(projectDir, "definitions", "setup_table.sqlx"), + writeDefinitionFile( + projectDir, + "setup_table.sqlx", ` config { type: "operations" @@ -699,9 +479,9 @@ CREATE OR REPLACE TABLE \`\${dataform.projectConfig.defaultDatabase}.\${dataform ` ); - fs.ensureFileSync(path.join(projectDir, "definitions", "example_incremental.sqlx")); - fs.writeFileSync( - path.join(projectDir, "definitions", "example_incremental.sqlx"), + writeDefinitionFile( + projectDir, + "example_incremental.sqlx", ` config { type: "incremental", @@ -711,9 +491,9 @@ SELECT 1 as id, 'new' as field1, 'new2' as field2 ` ); - fs.ensureFileSync(path.join(projectDir, "definitions", "teardown_schema.sqlx")); - fs.writeFileSync( - path.join(projectDir, "definitions", "teardown_schema.sqlx"), + writeDefinitionFile( + projectDir, + "teardown_schema.sqlx", ` config { type: "operations" @@ -727,30 +507,28 @@ DROP SCHEMA IF EXISTS \`\${dataform.projectConfig.defaultDatabase}.\${dataform.p try { // Run setup operation to create the table in BigQuery. // Dataform will automatically create the uniqueDataset schema. - await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, "--actions=setup_table" - ]) + ] ); // Run the incremental table in dry-run mode. // Dataform will detect the table exists and generate the dynamic procedural SQL. - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + const runResult = await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, "--dry-run", "--json", "--actions=example_incremental" - ]) + ] ); expect(runResult.exitCode).equals(0); @@ -799,70 +577,67 @@ DROP SCHEMA IF EXISTS \`\${dataform.projectConfig.defaultDatabase}.\${dataform.p expect(statement).to.include("ADD COLUMN IF NOT EXISTS"); } finally { // Teardown the schema completely, regardless of test success or failure. - await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, "--actions=teardown_schema" - ]) + ] ); } }); }); - test("--timeout on run emits deprecation notice pointing to --execution-timeout", async () => { - // The notice fires in the run handler before compile. Yargs validation - // requires workflow_settings.yaml to exist, but compile can fail after that - // — we only assert on stderr for the notice line. - const projectDir = tmpDirFixture.createNewTmpDir(); - fs.writeFileSync( - path.join(projectDir, "workflow_settings.yaml"), - `defaultProject: ${INTEGRATION_TEST_PROJECT}\ndefaultLocation: ${INTEGRATION_TEST_LOCATION}\n` - ); + suite("run --timeout deprecation", ({ beforeEach }) => { + let projectDir: string; - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--timeout", - "30s" - ]) - ); + beforeEach("setup test project", () => { + projectDir = tmpDirFixture.createNewTmpDir(); + fs.writeFileSync( + path.join(projectDir, "workflow_settings.yaml"), + `defaultProject: ${INTEGRATION_TEST_PROJECT}\ndefaultLocation: ${INTEGRATION_TEST_LOCATION}\n` + ); + }); - expect(runResult.stderr).to.match( - /--timeout only bounds project compilation[\s\S]*use --execution-timeout/ - ); - }); + test("--timeout on run emits deprecation notice pointing to --execution-timeout", async () => { + // The notice fires in the run handler before compile. Yargs validation + // requires workflow_settings.yaml to exist, but compile can fail after that + // — we only assert on stderr for the notice line. + const runResult = await runCli( + "run", + [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--timeout", + "30s" + ] + ); - test("--timeout on run does NOT emit notice when --execution-timeout is also set", async () => { - const projectDir = tmpDirFixture.createNewTmpDir(); - fs.writeFileSync( - path.join(projectDir, "workflow_settings.yaml"), - `defaultProject: ${INTEGRATION_TEST_PROJECT}\ndefaultLocation: ${INTEGRATION_TEST_LOCATION}\n` - ); + expect(runResult.stderr).to.match( + /--timeout only bounds project compilation[\s\S]*use --execution-timeout/ + ); + }); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, + test("--timeout on run does NOT emit notice when --execution-timeout is also set", async () => { + const runResult = await runCli( "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--timeout", - "30s", - "--execution-timeout", - "10m" - ]) - ); + [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--timeout", + "30s", + "--execution-timeout", + "10m" + ] + ); - expect(runResult.stderr).to.not.match( - /--timeout only bounds project compilation/ - ); - }); + expect(runResult.stderr).to.not.match( + /--timeout only bounds project compilation/ + ); + }); + }) }); diff --git a/cli/index_test_base.ts b/cli/index_test_base.ts index 861e36bee..6f94cb42c 100644 --- a/cli/index_test_base.ts +++ b/cli/index_test_base.ts @@ -1,5 +1,5 @@ // tslint:disable tsr-detect-non-literal-fs-filename -import { execFile } from "child_process"; +import { execFile, ExecFileOptions } from "child_process"; import * as fs from "fs-extra"; import { dump as dumpYaml, load as loadYaml } from "js-yaml"; import * as path from "path"; @@ -59,24 +59,28 @@ export const INTEGRATION_TEST_RESERVATION = `projects/${INTEGRATION_TEST_PROJECT export const cliEntryPointPath = "cli/node_modules/@dataform/cli/bundle.js"; -export async function setupJitProject( +export async function setupProject( tmpDirFixture: TmpDirFixture, - projectDir: string -): Promise { + projectDir: string, + workflowSettingsOverrides?: Partial +): Promise { const npmCacheDir = tmpDirFixture.createNewTmpDir(); + const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); const packageJsonPath = path.join(projectDir, "package.json"); - await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "init", projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]) - ); + // Initialize a project using the CLI, don't install packages. + await runCli("init", [projectDir, INTEGRATION_TEST_PROJECT, INTEGRATION_TEST_LOCATION]); - const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); - const workflowSettings = dataform.WorkflowSettings.create( - loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) - ); + // Install packages manually to get around bazel read-only sandbox issues. + const workflowSettings = dataform.WorkflowSettings.create({ + ...loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) as dataform.IWorkflowSettings, + ...workflowSettingsOverrides + }); delete workflowSettings.dataformCoreVersion; - fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings)); - + fs.writeFileSync( + workflowSettingsPath, + dumpYaml(dataform.WorkflowSettings.toObject(workflowSettings, { enums: String })) + ); fs.writeFileSync( packageJsonPath, `{ @@ -96,10 +100,54 @@ export async function setupJitProject( ]) ); - const jitTablePath = path.join(projectDir, "definitions", "jit_table.js"); - fs.ensureFileSync(jitTablePath); + return projectDir; +} + +export async function runCli( + cmd: string, + options: string[] = [], + execOptions?: ExecFileOptions +): Promise<{ + exitCode: number; + stdout: string; + stderr: string; +}> { + const args = [cliEntryPointPath, cmd, ...options]; + return getProcessResult( + execFile(nodePath, args, execOptions) + ); +} + +export function writeDefinitionFile(projectDir: string, filename: string, content: string): void { + const fullPath = path.join(projectDir, "definitions", filename); + fs.ensureFileSync(fullPath); + fs.writeFileSync(fullPath, content); +} + +export function alterWorkflowSettings( + projectDir: string, + workflowSettingsOverrides: Partial +): void { + const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml"); + const existingSettings = loadYaml(fs.readFileSync(workflowSettingsPath, "utf8")) as dataform.IWorkflowSettings; + const workflowSettings = dataform.WorkflowSettings.create({ + ...existingSettings, + ...workflowSettingsOverrides + }); fs.writeFileSync( - jitTablePath, + workflowSettingsPath, + dumpYaml(dataform.WorkflowSettings.toObject(workflowSettings, { enums: String })) + ); +} + +export async function setupJitProject( + tmpDirFixture: TmpDirFixture, + projectDir: string +): Promise { + await setupProject(tmpDirFixture, projectDir); + writeDefinitionFile( + projectDir, + "jit_table.js", `publish("jit_table", {type: "table"}).jitCode(async (ctx) => { return "SELECT 1 as id"; })` ); } diff --git a/cli/tests/jit/index_jit_advanced_test.ts b/cli/tests/jit/index_jit_advanced_test.ts index 01c274fa4..df9f8e656 100644 --- a/cli/tests/jit/index_jit_advanced_test.ts +++ b/cli/tests/jit/index_jit_advanced_test.ts @@ -1,14 +1,12 @@ import { expect } from "chai"; -import { execFile } from "child_process"; -import * as fs from "fs-extra"; -import * as path from "path"; import { - cliEntryPointPath, CREDENTIALS_PATH, - setupJitProject + runCli, + setupJitProject, + writeDefinitionFile } from "df/cli/index_test_base"; -import { getProcessResult, nodePath, suite, test } from "df/testing"; +import { suite, test } from "df/testing"; import { TmpDirFixture } from "df/testing/fixtures"; suite("JiT support advanced", ({ afterEach }) => { @@ -17,9 +15,9 @@ suite("JiT support advanced", ({ afterEach }) => { test("JiT preOps and postOps support", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const prePostPath = path.join(projectDir, "definitions", "pre_post_jit.js"); - fs.writeFileSync( - prePostPath, + writeDefinitionFile( + projectDir, + "pre_post_jit.js", `publish("pre_post_jit", { type: "table" }).jitCode(async (jctx) => { return { query: "SELECT 1 as id", @@ -29,18 +27,14 @@ suite("JiT support advanced", ({ afterEach }) => { })` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=pre_post_jit" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=pre_post_jit" + ]); expect(runResult.exitCode).equals(0); const executedGraph = JSON.parse(runResult.stdout); @@ -54,9 +48,9 @@ suite("JiT support advanced", ({ afterEach }) => { test({ name: "JiT incremental pre/post ops support", timeout: 60000 }, async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const incPrePostPath = path.join(projectDir, "definitions", "inc_pre_post_jit.js"); - fs.writeFileSync( - incPrePostPath, + writeDefinitionFile( + projectDir, + "inc_pre_post_jit.js", `publish("inc_pre_post_jit", { type: "incremental" }).jitCode(async (jctx) => { if (jctx.incremental()) { return { @@ -72,19 +66,15 @@ suite("JiT support advanced", ({ afterEach }) => { })` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=inc_pre_post_jit", - "--full-refresh" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=inc_pre_post_jit", + "--full-refresh" + ]); expect(runResult.exitCode).equals(0); const executedGraph = JSON.parse(runResult.stdout); @@ -95,18 +85,14 @@ suite("JiT support advanced", ({ afterEach }) => { // Also validate when not using full-refresh. // Since the table doesn't exist, jctx.incremental() should still be false. - const runResultIncremental = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=inc_pre_post_jit" - ]) - ); + const runResultIncremental = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=inc_pre_post_jit" + ]); expect(runResultIncremental.exitCode).equals(0); const executedGraphInc = JSON.parse(runResultIncremental.stdout); @@ -119,9 +105,9 @@ suite("JiT support advanced", ({ afterEach }) => { test({ name: "JiT incremental mode validation with consecutive runs", timeout: 60000 }, async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const incPath = path.join(projectDir, "definitions", "inc_jit.js"); - fs.writeFileSync( - incPath, + writeDefinitionFile( + projectDir, + "inc_jit.js", `publish("inc_jit", { type: "incremental" }).jitCode(async (jctx) => { if (jctx.incremental()) { return { @@ -138,33 +124,25 @@ suite("JiT support advanced", ({ afterEach }) => { ); // 1. Initial run with full-refresh to create the table. - const firstRun = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--actions=inc_jit", - "--full-refresh" - ]) - ); + const firstRun = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--actions=inc_jit", + "--full-refresh" + ]); expect(firstRun.exitCode).equals(0); // 2. Second run without full-refresh. // The table now exists, so it should use the incremental path. - const secondRun = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=inc_jit" - ]) - ); + const secondRun = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=inc_jit" + ]); expect(secondRun.exitCode).equals(0); const secondGraph = JSON.parse(secondRun.stdout); @@ -177,30 +155,28 @@ suite("JiT support advanced", ({ afterEach }) => { test("JiT project-level data support", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - fs.writeFileSync( - path.join(projectDir, "definitions", "project_data.js"), + writeDefinitionFile( + projectDir, + "project_data.js", "const { session } = require('@dataform/core');\nsession.jitData('app_secret', 'e2e_secret_value');" ); - fs.writeFileSync( - path.join(projectDir, "definitions", "jit_data_test.js"), + writeDefinitionFile( + projectDir, + "jit_data_test.js", `publish("jit_data_test", { type: "table" }).jitCode(async (jctx) => { const secret = jctx.data.app_secret; return "SELECT '" + secret + "' as val"; })` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=jit_data_test" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=jit_data_test" + ]); expect(runResult.exitCode).equals(0); const executedGraph = JSON.parse(runResult.stdout); @@ -211,8 +187,9 @@ suite("JiT support advanced", ({ afterEach }) => { test("JiT complex session data support", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - fs.writeFileSync( - path.join(projectDir, "definitions", "complex_project_data.js"), + writeDefinitionFile( + projectDir, + "complex_project_data.js", "const { session } = require('@dataform/core');\n" + "session.jitData('app_config', {\n" + " env: 'test-env',\n" + @@ -220,26 +197,23 @@ suite("JiT support advanced", ({ afterEach }) => { " tags: ['t1', 't2']\n" + "});" ); - fs.writeFileSync( - path.join(projectDir, "definitions", "jit_complex_data_test.js"), + writeDefinitionFile( + projectDir, + "jit_complex_data_test.js", "publish('jit_complex_data_test', { type: 'table' }).jitCode(async (jctx) => {\n" + " const config = jctx.data.app_config;\n" + " return 'SELECT \\'' + config.env + '\\' as env, ' + config.version + ' as ver, \\'' + config.tags[0] + '\\' as tag';\n" + "})" ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=jit_complex_data_test" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=jit_complex_data_test" + ]); expect(runResult.exitCode).equals(0); const executedGraph = JSON.parse(runResult.stdout); diff --git a/cli/tests/jit/index_jit_dependency_test.ts b/cli/tests/jit/index_jit_dependency_test.ts index 3d50c65d9..6735e4102 100644 --- a/cli/tests/jit/index_jit_dependency_test.ts +++ b/cli/tests/jit/index_jit_dependency_test.ts @@ -1,15 +1,13 @@ import { expect } from "chai"; -import { execFile } from "child_process"; -import * as fs from "fs-extra"; -import * as path from "path"; import { - cliEntryPointPath, CREDENTIALS_PATH, INTEGRATION_TEST_PROJECT, - setupJitProject + runCli, + setupJitProject, + writeDefinitionFile } from "df/cli/index_test_base"; -import { getProcessResult, nodePath, suite, test } from "df/testing"; +import { suite, test } from "df/testing"; import { TmpDirFixture } from "df/testing/fixtures"; suite("JiT support dependencies", ({ afterEach }) => { @@ -19,31 +17,29 @@ suite("JiT support dependencies", ({ afterEach }) => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); // A (AoT) -> B (JiT) - fs.writeFileSync( - path.join(projectDir, "definitions", "table_a.sqlx"), + writeDefinitionFile( + projectDir, + "table_a.sqlx", "config { type: 'table' } SELECT 1 as val" ); - fs.writeFileSync( - path.join(projectDir, "definitions", "table_b.js"), + writeDefinitionFile( + projectDir, + "table_b.js", `publish("table_b", { type: "table", dependencies: ["table_a"] }).jitCode(async (jctx) => { const upstream = jctx.ref("table_a"); return "SELECT '" + upstream + "' as ref_name"; })` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=table_b", - "--include-deps" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=table_b", + "--include-deps" + ]); expect(runResult.exitCode).equals(0); const executedGraph = JSON.parse(runResult.stdout); @@ -59,31 +55,29 @@ suite("JiT support dependencies", ({ afterEach }) => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); // Action A (JiT) -> Action B (JiT) - fs.writeFileSync( - path.join(projectDir, "definitions", "jit_a.js"), + writeDefinitionFile( + projectDir, + "jit_a.js", 'publish("jit_a", { type: "table" }).jitCode(async () => "SELECT 1 as val")' ); - fs.writeFileSync( - path.join(projectDir, "definitions", "jit_b.js"), + writeDefinitionFile( + projectDir, + "jit_b.js", "publish('jit_b', { type: 'table', dependencies: ['jit_a'] }).jitCode(async (jctx) => {\n" + " const upstream = jctx.ref('jit_a');\n" + " return 'SELECT \\'' + upstream + '\\' as ref_name';\n" + "})" ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=jit_b", - "--include-deps" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=jit_b", + "--include-deps" + ]); expect(runResult.exitCode).equals(0); const executedGraph = JSON.parse(runResult.stdout); diff --git a/cli/tests/jit/index_jit_main_test.ts b/cli/tests/jit/index_jit_main_test.ts index 3a86732b2..b3f264b46 100644 --- a/cli/tests/jit/index_jit_main_test.ts +++ b/cli/tests/jit/index_jit_main_test.ts @@ -1,15 +1,13 @@ import { expect } from "chai"; -import { execFile } from "child_process"; -import * as fs from "fs-extra"; -import * as path from "path"; import { - cliEntryPointPath, CREDENTIALS_PATH, INTEGRATION_TEST_PROJECT, - setupJitProject + runCli, + setupJitProject, + writeDefinitionFile } from "df/cli/index_test_base"; -import { getProcessResult, nodePath, suite, test } from "df/testing"; +import { suite, test } from "df/testing"; import { TmpDirFixture } from "df/testing/fixtures"; suite("JiT support main", ({ afterEach }) => { @@ -18,9 +16,7 @@ suite("JiT support main", ({ afterEach }) => { test("compile command includes jitCode in output", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const compileResult = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir, "--json"]) - ); + const compileResult = await runCli("compile", [projectDir, "--json"]); expect(compileResult.exitCode).equals(0); const compiledGraph = JSON.parse(compileResult.stdout); @@ -34,15 +30,13 @@ suite("JiT support main", ({ afterEach }) => { test("fails if both query and jitCode are provided", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const conflictPath = path.join(projectDir, "definitions", "conflict.js"); - fs.writeFileSync( - conflictPath, + writeDefinitionFile( + projectDir, + "conflict.js", `publish("conflict", {type: "table"}).query("SELECT 1").jitCode(async (ctx) => "SELECT 2")` ); - const compileResult = await getProcessResult( - execFile(nodePath, [cliEntryPointPath, "compile", projectDir, "--json"]) - ); + const compileResult = await runCli("compile", [projectDir, "--json"]); expect(compileResult.exitCode).equals(1); expect(compileResult.stderr).to.include("Cannot mix AoT and JiT compilation in action"); @@ -52,18 +46,14 @@ suite("JiT support main", ({ afterEach }) => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=jit_table" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=jit_table" + ]); expect(runResult.exitCode).equals(0); @@ -78,20 +68,15 @@ suite("JiT support main", ({ afterEach }) => { test("mixed AoT and JiT support", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const aotTablePath = path.join(projectDir, "definitions", "aot_table.sqlx"); - fs.writeFileSync(aotTablePath, "config { type: 'table' } SELECT 2 as id"); + writeDefinitionFile(projectDir, "aot_table.sqlx", "config { type: 'table' } SELECT 2 as id"); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json" + ]); expect(runResult.exitCode).equals(0); @@ -113,26 +98,25 @@ suite("JiT support main", ({ afterEach }) => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const disabledPath = path.join(projectDir, "definitions", "disabled_jit.js"); - fs.writeFileSync( - disabledPath, + writeDefinitionFile( + projectDir, + "disabled_jit.js", `publish("disabled_jit", { type: "table", disabled: true }).jitCode(async (jctx) => { throw new Error("Should not be executed"); })` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + const runResult = await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, - "--actions=disabled_jit"], - { - env: { ...process.env, NO_COLOR: "1" } - } - ) + "--actions=disabled_jit" + ], + { + env: { ...process.env, NO_COLOR: "1" } + } ); expect(runResult.exitCode).equals(0); @@ -143,24 +127,20 @@ suite("JiT support main", ({ afterEach }) => { test("JiT compilation failure reporting", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const failingJitPath = path.join(projectDir, "definitions", "failing_jit.js"); - fs.writeFileSync( - failingJitPath, + writeDefinitionFile( + projectDir, + "failing_jit.js", `publish("failing_jit", {type: "table"}).jitCode(async (ctx) => { throw new Error("JiT compilation failed!"); })` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=failing_jit" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=failing_jit" + ]); expect(runResult.exitCode).equals(1); @@ -176,9 +156,9 @@ suite("JiT support main", ({ afterEach }) => { test("surfaces 'Table not found' RPC error during JiT compilation", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const rpcJitPath = path.join(projectDir, "definitions", "rpc_jit.js"); - fs.writeFileSync( - rpcJitPath, + writeDefinitionFile( + projectDir, + "rpc_jit.js", `publish("rpc_jit", {type: "table"}).jitCode(async (jctx) => { // This will fail because the table does not exist in the warehouse, // and jctx.adapter.getTable throws an error in this case. @@ -187,18 +167,14 @@ suite("JiT support main", ({ afterEach }) => { })` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=rpc_jit" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=rpc_jit" + ]); expect(runResult.exitCode).equals(1); @@ -218,21 +194,16 @@ suite("JiT support main", ({ afterEach }) => { test("mixed support with AoT filtered out", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const aotTablePath = path.join(projectDir, "definitions", "aot_table.sqlx"); - fs.writeFileSync(aotTablePath, "config { type: 'table' } SELECT 2 as id"); + writeDefinitionFile(projectDir, "aot_table.sqlx", "config { type: 'table' } SELECT 2 as id"); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=jit_table" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=jit_table" + ]); expect(runResult.exitCode).equals(0); @@ -247,21 +218,16 @@ suite("JiT support main", ({ afterEach }) => { test("mixed support with JiT filtered out", async () => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - const aotTablePath = path.join(projectDir, "definitions", "aot_table.sqlx"); - fs.writeFileSync(aotTablePath, "config { type: 'table' } SELECT 2 as id"); - - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=aot_table" - ]) - ); + writeDefinitionFile(projectDir, "aot_table.sqlx", "config { type: 'table' } SELECT 2 as id"); + + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=aot_table" + ]); expect(runResult.exitCode).equals(0); diff --git a/cli/tests/jit/index_jit_runtime_test.ts b/cli/tests/jit/index_jit_runtime_test.ts index eb27f5785..a56b8687a 100644 --- a/cli/tests/jit/index_jit_runtime_test.ts +++ b/cli/tests/jit/index_jit_runtime_test.ts @@ -1,14 +1,14 @@ import { expect } from "chai"; -import { execFile } from "child_process"; import * as fs from "fs-extra"; import * as path from "path"; import { - cliEntryPointPath, CREDENTIALS_PATH, - setupJitProject + runCli, + setupJitProject, + writeDefinitionFile } from "df/cli/index_test_base"; -import { getProcessResult, nodePath, suite, test } from "df/testing"; +import { suite, test } from "df/testing"; import { TmpDirFixture } from "df/testing/fixtures"; suite("JiT support runtime", ({ afterEach }) => { @@ -24,26 +24,23 @@ suite("JiT support runtime", ({ afterEach }) => { "module.exports = { getValue: () => 'required_value' };" ); // Add a JiT table that requires it - fs.writeFileSync( - path.join(projectDir, "definitions", "jit_require_test.js"), + writeDefinitionFile( + projectDir, + "jit_require_test.js", `publish("jit_require_test", { type: "table" }).jitCode(async (jctx) => { const utils = require("../helpers/utils.js"); return "SELECT '" + utils.getValue() + "' as val"; })` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=jit_require_test" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=jit_require_test" + ]); expect(runResult.exitCode).equals(1); expect(runResult.stdout).to.match(/Cannot find module/i); @@ -54,18 +51,17 @@ suite("JiT support runtime", ({ afterEach }) => { await setupJitProject(tmpDirFixture, projectDir); // Add a JiT table that hangs in an infinite loop - const hangPath = path.join(projectDir, "definitions", "hang_jit.js"); - fs.writeFileSync( - hangPath, + writeDefinitionFile( + projectDir, + "hang_jit.js", `publish("hang_jit", { type: "table" }).jitCode(async (jctx) => { while(true) { /* loop */ } return "SELECT 1"; })` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + const runResult = await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, @@ -73,7 +69,8 @@ suite("JiT support runtime", ({ afterEach }) => { "--json", "--actions=hang_jit", "--jit-timeout=4s" - ], { timeout: 50000 }) + ], + { timeout: 50000 } ); expect(runResult.exitCode).equals(1); @@ -84,8 +81,9 @@ suite("JiT support runtime", ({ afterEach }) => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - fs.writeFileSync( - path.join(projectDir, "definitions", "hang_jit_global.js"), + writeDefinitionFile( + projectDir, + "hang_jit_global.js", `publish("hang_jit_global", { type: "table" }).jitCode(async (jctx) => { while(true) { /* loop */ } return "SELECT 1"; @@ -94,10 +92,9 @@ suite("JiT support runtime", ({ afterEach }) => { // --execution-timeout must exceed BQ schema-prep time; smaller values fire // the timer before the JiT compile starts, leaving the action SKIPPED and // defeating the assertions below. - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", + const runResult = await runCli( + "run", + [ projectDir, "--credentials", CREDENTIALS_PATH, @@ -105,7 +102,8 @@ suite("JiT support runtime", ({ afterEach }) => { "--json", "--actions=hang_jit_global", "--execution-timeout=15s" - ], { timeout: 80000 }) + ], + { timeout: 80000 } ); expect(runResult.exitCode).equals(1); @@ -124,23 +122,20 @@ suite("JiT support runtime", ({ afterEach }) => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); - fs.writeFileSync( - path.join(projectDir, "definitions", "jit_assertion.js"), + writeDefinitionFile( + projectDir, + "jit_assertion.js", `assert("jit_assertion").jitCode(async (jctx) => "SELECT 1 as row_count")` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=jit_assertion" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=jit_assertion" + ]); expect(runResult.exitCode).equals(0); const executedGraph = JSON.parse(runResult.stdout); @@ -157,23 +152,20 @@ suite("JiT support runtime", ({ afterEach }) => { await setupJitProject(tmpDirFixture, projectDir); // Add multiple JiT tables for (let i = 0; i < 5; i++) { - fs.writeFileSync( - path.join(projectDir, "definitions", `jit_${i}.js`), + writeDefinitionFile( + projectDir, + `jit_${i}.js`, `publish("jit_${i}", { type: "table" }).jitCode(async (jctx) => "SELECT ${i} as val")` ); } - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json" + ]); expect(runResult.exitCode).equals(0); const executedGraph = JSON.parse(runResult.stdout); @@ -184,27 +176,23 @@ suite("JiT support runtime", ({ afterEach }) => { const projectDir = tmpDirFixture.createNewTmpDir(); await setupJitProject(tmpDirFixture, projectDir); // Add a JiT table that crashes the process - const crashPath = path.join(projectDir, "definitions", "crash_jit.js"); - fs.writeFileSync( - crashPath, + writeDefinitionFile( + projectDir, + "crash_jit.js", `publish("crash_jit", { type: "table" }).jitCode(async (jctx) => { setTimeout(() => { throw new Error("Hard crash"); }, 10); return new Promise(() => {}); // Hang until crash })` ); - const runResult = await getProcessResult( - execFile(nodePath, [ - cliEntryPointPath, - "run", - projectDir, - "--credentials", - CREDENTIALS_PATH, - "--dry-run", - "--json", - "--actions=crash_jit" - ]) - ); + const runResult = await runCli("run", [ + projectDir, + "--credentials", + CREDENTIALS_PATH, + "--dry-run", + "--json", + "--actions=crash_jit" + ]); expect(runResult.exitCode).equals(1); const executedGraph = JSON.parse(runResult.stdout);