import * as fs from "fs"; import { ExecOptions } from "@actions/exec"; import * as toolrunner from "@actions/exec/lib/toolrunner"; import * as toolcache from "@actions/tool-cache"; import * as safeWhich from "@chrisgavin/safe-which"; import test, { ExecutionContext } from "ava"; import del from "del"; import * as yaml from "js-yaml"; import nock from "nock"; import * as sinon from "sinon"; import * as actionsUtil from "./actions-util"; import { GitHubApiDetails } from "./api-client"; import { CommandInvocationError } from "./cli-errors"; import * as codeql from "./codeql"; import { AugmentationProperties, Config } from "./config-utils"; import * as defaults from "./defaults.json"; import { DocUrl } from "./doc-url"; import { Language } from "./languages"; import { getRunnerLogger } from "./logging"; import { ToolsSource } from "./setup-codeql"; import { setupTests, createFeatures, setupActionsVars, SAMPLE_DOTCOM_API_DETAILS, SAMPLE_DEFAULT_CLI_VERSION, mockBundleDownloadApi, makeVersionInfo, createTestConfig, } from "./testing-utils"; import { ToolsFeature } from "./tools-features"; import * as util from "./util"; import { initializeEnvironment } from "./util"; setupTests(test); let stubConfig: Config; test.beforeEach(() => { initializeEnvironment("1.2.3"); stubConfig = createTestConfig({ languages: [Language.cpp], }); }); async function installIntoToolcache({ apiDetails = SAMPLE_DOTCOM_API_DETAILS, cliVersion, isPinned, tagName, tmpDir, }: { apiDetails?: GitHubApiDetails; cliVersion?: string; isPinned: boolean; tagName: string; tmpDir: string; }) { const url = mockBundleDownloadApi({ apiDetails, isPinned, tagName }); await codeql.setupCodeQL( cliVersion !== undefined ? undefined : url, apiDetails, tmpDir, util.GitHubVariant.GHES, cliVersion !== undefined ? { cliVersion, tagName } : SAMPLE_DEFAULT_CLI_VERSION, getRunnerLogger(true), false, ); } function mockReleaseApi({ apiDetails = SAMPLE_DOTCOM_API_DETAILS, assetNames, tagName, }: { apiDetails?: GitHubApiDetails; assetNames: string[]; tagName: string; }): nock.Scope { return nock(apiDetails.apiURL!) .get(`/repos/github/codeql-action/releases/tags/${tagName}`) .reply(200, { assets: assetNames.map((name) => ({ name, })), tag_name: tagName, }); } function mockApiDetails(apiDetails: GitHubApiDetails) { // This is a workaround to mock `api.getApiDetails()` since it doesn't seem to be possible to // mock this directly. The difficulty is that `getApiDetails()` is called locally in // `api-client.ts`, but `sinon.stub(api, "getApiDetails")` only affects calls to // `getApiDetails()` via an imported `api` module. sinon .stub(actionsUtil, "getRequiredInput") .withArgs("token") .returns(apiDetails.auth); const requiredEnvParamStub = sinon.stub(util, "getRequiredEnvParam"); requiredEnvParamStub.withArgs("GITHUB_SERVER_URL").returns(apiDetails.url); requiredEnvParamStub .withArgs("GITHUB_API_URL") .returns(apiDetails.apiURL || ""); } test("downloads and caches explicitly requested bundles that aren't in the toolcache", async (t) => { await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); const versions = ["20200601", "20200610"]; for (let i = 0; i < versions.length; i++) { const version = versions[i]; const url = mockBundleDownloadApi({ tagName: `codeql-bundle-${version}`, isPinned: false, }); const result = await codeql.setupCodeQL( url, SAMPLE_DOTCOM_API_DETAILS, tmpDir, util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, getRunnerLogger(true), false, ); t.assert(toolcache.find("CodeQL", `0.0.0-${version}`)); t.is(result.toolsVersion, `0.0.0-${version}`); t.is(result.toolsSource, ToolsSource.Download); t.assert( Number.isInteger(result.toolsDownloadStatusReport?.downloadDurationMs), ); } t.is(toolcache.findAllVersions("CodeQL").length, 2); }); }); test("caches semantically versioned bundles using their semantic version number", async (t) => { await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); const url = mockBundleDownloadApi({ tagName: `codeql-bundle-v2.14.0`, isPinned: false, }); const result = await codeql.setupCodeQL( url, SAMPLE_DOTCOM_API_DETAILS, tmpDir, util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, getRunnerLogger(true), false, ); t.is(toolcache.findAllVersions("CodeQL").length, 1); t.assert(toolcache.find("CodeQL", `2.14.0`)); t.is(result.toolsVersion, `2.14.0`); t.is(result.toolsSource, ToolsSource.Download); t.assert( Number.isInteger(result.toolsDownloadStatusReport?.downloadDurationMs), ); }); }); test("downloads an explicitly requested bundle even if a different version is cached", async (t) => { await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); await installIntoToolcache({ tagName: "codeql-bundle-20200601", isPinned: true, tmpDir, }); const url = mockBundleDownloadApi({ tagName: "codeql-bundle-20200610", }); const result = await codeql.setupCodeQL( url, SAMPLE_DOTCOM_API_DETAILS, tmpDir, util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, getRunnerLogger(true), false, ); t.assert(toolcache.find("CodeQL", "0.0.0-20200610")); t.deepEqual(result.toolsVersion, "0.0.0-20200610"); t.is(result.toolsSource, ToolsSource.Download); t.assert( Number.isInteger(result.toolsDownloadStatusReport?.downloadDurationMs), ); }); }); const EXPLICITLY_REQUESTED_BUNDLE_TEST_CASES = [ { tagName: "codeql-bundle-2.17.6", expectedToolcacheVersion: "2.17.6", }, { tagName: "codeql-bundle-20240805", expectedToolcacheVersion: "0.0.0-20240805", }, ]; for (const { tagName, expectedToolcacheVersion, } of EXPLICITLY_REQUESTED_BUNDLE_TEST_CASES) { test(`caches explicitly requested bundle ${tagName} as ${expectedToolcacheVersion}`, async (t) => { await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); mockApiDetails(SAMPLE_DOTCOM_API_DETAILS); sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); const url = mockBundleDownloadApi({ tagName, }); const result = await codeql.setupCodeQL( url, SAMPLE_DOTCOM_API_DETAILS, tmpDir, util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, getRunnerLogger(true), false, ); t.assert(toolcache.find("CodeQL", expectedToolcacheVersion)); t.deepEqual(result.toolsVersion, expectedToolcacheVersion); t.is(result.toolsSource, ToolsSource.Download); t.assert( Number.isInteger(result.toolsDownloadStatusReport?.downloadDurationMs), ); }); }); } for (const toolcacheVersion of [ // Test that we use the tools from the toolcache when `SAMPLE_DEFAULT_CLI_VERSION` is requested // and `SAMPLE_DEFAULT_CLI_VERSION-` is in the toolcache. SAMPLE_DEFAULT_CLI_VERSION.cliVersion, `${SAMPLE_DEFAULT_CLI_VERSION.cliVersion}-20230101`, ]) { test( `uses tools from toolcache when ${SAMPLE_DEFAULT_CLI_VERSION.cliVersion} is requested and ` + `${toolcacheVersion} is installed`, async (t) => { await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); sinon .stub(toolcache, "find") .withArgs("CodeQL", toolcacheVersion) .returns("path/to/cached/codeql"); sinon.stub(toolcache, "findAllVersions").returns([toolcacheVersion]); const result = await codeql.setupCodeQL( undefined, SAMPLE_DOTCOM_API_DETAILS, tmpDir, util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, getRunnerLogger(true), false, ); t.is(result.toolsVersion, SAMPLE_DEFAULT_CLI_VERSION.cliVersion); t.is(result.toolsSource, ToolsSource.Toolcache); t.is(result.toolsDownloadStatusReport?.downloadDurationMs, undefined); }); }, ); } test(`uses a cached bundle when no tools input is given on GHES`, async (t) => { await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); await installIntoToolcache({ tagName: "codeql-bundle-20200601", isPinned: true, tmpDir, }); const result = await codeql.setupCodeQL( undefined, SAMPLE_DOTCOM_API_DETAILS, tmpDir, util.GitHubVariant.GHES, { cliVersion: defaults.cliVersion, tagName: defaults.bundleVersion, }, getRunnerLogger(true), false, ); t.deepEqual(result.toolsVersion, "0.0.0-20200601"); t.is(result.toolsSource, ToolsSource.Toolcache); t.is(result.toolsDownloadStatusReport?.downloadDurationMs, undefined); const cachedVersions = toolcache.findAllVersions("CodeQL"); t.is(cachedVersions.length, 1); }); }); test(`downloads bundle if only an unpinned version is cached on GHES`, async (t) => { await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); await installIntoToolcache({ tagName: "codeql-bundle-20200601", isPinned: false, tmpDir, }); mockBundleDownloadApi({ tagName: defaults.bundleVersion, }); const result = await codeql.setupCodeQL( undefined, SAMPLE_DOTCOM_API_DETAILS, tmpDir, util.GitHubVariant.GHES, { cliVersion: defaults.cliVersion, tagName: defaults.bundleVersion, }, getRunnerLogger(true), false, ); t.deepEqual(result.toolsVersion, defaults.cliVersion); t.is(result.toolsSource, ToolsSource.Download); t.assert( Number.isInteger(result.toolsDownloadStatusReport?.downloadDurationMs), ); const cachedVersions = toolcache.findAllVersions("CodeQL"); t.is(cachedVersions.length, 2); }); }); test('downloads bundle if "latest" tools specified but not cached', async (t) => { await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); await installIntoToolcache({ tagName: "codeql-bundle-20200601", isPinned: true, tmpDir, }); mockBundleDownloadApi({ tagName: defaults.bundleVersion, }); const result = await codeql.setupCodeQL( "latest", SAMPLE_DOTCOM_API_DETAILS, tmpDir, util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, getRunnerLogger(true), false, ); t.deepEqual(result.toolsVersion, defaults.cliVersion); t.is(result.toolsSource, ToolsSource.Download); t.assert( Number.isInteger(result.toolsDownloadStatusReport?.downloadDurationMs), ); const cachedVersions = toolcache.findAllVersions("CodeQL"); t.is(cachedVersions.length, 2); }); }); test("bundle URL from another repo is cached as 0.0.0-bundleVersion", async (t) => { await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); mockApiDetails(SAMPLE_DOTCOM_API_DETAILS); sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); const releasesApiMock = mockReleaseApi({ assetNames: ["cli-version-2.13.5.txt"], tagName: "codeql-bundle-20230203", }); mockBundleDownloadApi({ repo: "codeql-testing/codeql-cli-nightlies", platformSpecific: false, tagName: "codeql-bundle-20230203", }); const result = await codeql.setupCodeQL( "https://github.com/codeql-testing/codeql-cli-nightlies/releases/download/codeql-bundle-20230203/codeql-bundle.tar.gz", SAMPLE_DOTCOM_API_DETAILS, tmpDir, util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, getRunnerLogger(true), false, ); t.is(result.toolsVersion, "0.0.0-20230203"); t.is(result.toolsSource, ToolsSource.Download); t.true( Number.isInteger(result.toolsDownloadStatusReport?.downloadDurationMs), ); const cachedVersions = toolcache.findAllVersions("CodeQL"); t.is(cachedVersions.length, 1); t.is(cachedVersions[0], "0.0.0-20230203"); t.false(releasesApiMock.isDone()); }); }); test("getExtraOptions works for explicit paths", (t) => { t.deepEqual(codeql.getExtraOptions({}, ["foo"], []), []); t.deepEqual(codeql.getExtraOptions({ foo: [42] }, ["foo"], []), ["42"]); t.deepEqual( codeql.getExtraOptions({ foo: { bar: [42] } }, ["foo", "bar"], []), ["42"], ); }); test("getExtraOptions works for wildcards", (t) => { t.deepEqual(codeql.getExtraOptions({ "*": [42] }, ["foo"], []), ["42"]); }); test("getExtraOptions works for wildcards and explicit paths", (t) => { const o1 = { "*": [42], foo: [87] }; t.deepEqual(codeql.getExtraOptions(o1, ["foo"], []), ["42", "87"]); const o2 = { "*": [42], foo: [87] }; t.deepEqual(codeql.getExtraOptions(o2, ["foo", "bar"], []), ["42"]); const o3 = { "*": [42], foo: { "*": [87], bar: [99] } }; const p = ["foo", "bar"]; t.deepEqual(codeql.getExtraOptions(o3, p, []), ["42", "87", "99"]); }); test("getExtraOptions throws for bad content", (t) => { t.throws(() => codeql.getExtraOptions({ "*": 42 }, ["foo"], [])); t.throws(() => codeql.getExtraOptions({ foo: 87 }, ["foo"], [])); t.throws(() => codeql.getExtraOptions( { "*": [42], foo: { "*": 87, bar: [99] } }, ["foo", "bar"], [], ), ); }); // Test macro for ensuring different variants of injected augmented configurations const injectedConfigMacro = test.macro({ exec: async ( t: ExecutionContext, augmentationProperties: AugmentationProperties, configOverride: Partial, expectedConfig: any, ) => { await util.withTmpDir(async (tempDir) => { const runnerConstructorStub = stubToolRunnerConstructor(); const codeqlObject = await codeql.getCodeQLForTesting(); sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("1.0.0")); const thisStubConfig: Config = { ...stubConfig, ...configOverride, tempDir, augmentationProperties, }; await codeqlObject.databaseInitCluster( thisStubConfig, "", undefined, undefined, getRunnerLogger(true), ); const args = runnerConstructorStub.firstCall.args[1] as string[]; // should have used an config file const configArg = args.find((arg: string) => arg.startsWith("--codescanning-config="), ); t.truthy(configArg, "Should have injected a codescanning config"); const configFile = configArg!.split("=")[1]; const augmentedConfig = yaml.load(fs.readFileSync(configFile, "utf8")); t.deepEqual(augmentedConfig, expectedConfig); await del(configFile, { force: true }); }); }, title: (providedTitle = "") => `databaseInitCluster() injected config: ${providedTitle}`, }); test( "basic", injectedConfigMacro, { queriesInputCombines: false, packsInputCombines: false, }, {}, {}, ); test( "injected packs from input", injectedConfigMacro, { queriesInputCombines: false, packsInputCombines: false, packsInput: ["xxx", "yyy"], }, {}, { packs: ["xxx", "yyy"], }, ); test( "injected packs from input with existing packs combines", injectedConfigMacro, { queriesInputCombines: false, packsInputCombines: true, packsInput: ["xxx", "yyy"], }, { originalUserInput: { packs: { cpp: ["codeql/something-else"], }, }, }, { packs: { cpp: ["codeql/something-else", "xxx", "yyy"], }, }, ); test( "injected packs from input with existing packs overrides", injectedConfigMacro, { queriesInputCombines: false, packsInputCombines: false, packsInput: ["xxx", "yyy"], }, { originalUserInput: { packs: { cpp: ["codeql/something-else"], }, }, }, { packs: ["xxx", "yyy"], }, ); // similar, but with queries test( "injected queries from input", injectedConfigMacro, { queriesInputCombines: false, packsInputCombines: false, queriesInput: [{ uses: "xxx" }, { uses: "yyy" }], }, {}, { queries: [ { uses: "xxx", }, { uses: "yyy", }, ], }, ); test( "injected queries from input overrides", injectedConfigMacro, { queriesInputCombines: false, packsInputCombines: false, queriesInput: [{ uses: "xxx" }, { uses: "yyy" }], }, { originalUserInput: { queries: [{ uses: "zzz" }], }, }, { queries: [ { uses: "xxx", }, { uses: "yyy", }, ], }, ); test( "injected queries from input combines", injectedConfigMacro, { queriesInputCombines: true, packsInputCombines: false, queriesInput: [{ uses: "xxx" }, { uses: "yyy" }], }, { originalUserInput: { queries: [{ uses: "zzz" }], }, }, { queries: [ { uses: "zzz", }, { uses: "xxx", }, { uses: "yyy", }, ], }, ); test( "injected queries from input combines 2", injectedConfigMacro, { queriesInputCombines: true, packsInputCombines: true, queriesInput: [{ uses: "xxx" }, { uses: "yyy" }], }, {}, { queries: [ { uses: "xxx", }, { uses: "yyy", }, ], }, ); test( "injected queries and packs, but empty", injectedConfigMacro, { queriesInputCombines: true, packsInputCombines: true, queriesInput: [], packsInput: [], }, { originalUserInput: { packs: [], queries: [], }, }, {}, ); test("passes a code scanning config AND qlconfig to the CLI", async (t: ExecutionContext) => { await util.withTmpDir(async (tempDir) => { const runnerConstructorStub = stubToolRunnerConstructor(); const codeqlObject = await codeql.getCodeQLForTesting(); sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6")); await codeqlObject.databaseInitCluster( { ...stubConfig, tempDir }, "", undefined, "/path/to/qlconfig.yml", getRunnerLogger(true), ); const args = runnerConstructorStub.firstCall.args[1] as string[]; // should have used a config file const hasCodeScanningConfigArg = args.some((arg: string) => arg.startsWith("--codescanning-config="), ); t.true(hasCodeScanningConfigArg, "Should have injected a qlconfig"); // should have passed a qlconfig file const hasQlconfigArg = args.some((arg: string) => arg.startsWith("--qlconfig-file="), ); t.truthy(hasQlconfigArg, "Should have injected a codescanning config"); }); }); test("does not pass a qlconfig to the CLI when it is undefined", async (t: ExecutionContext) => { await util.withTmpDir(async (tempDir) => { const runnerConstructorStub = stubToolRunnerConstructor(); const codeqlObject = await codeql.getCodeQLForTesting(); sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6")); await codeqlObject.databaseInitCluster( { ...stubConfig, tempDir }, "", undefined, undefined, // undefined qlconfigFile getRunnerLogger(true), ); const args = runnerConstructorStub.firstCall.args[1] as any[]; const hasQlconfigArg = args.some((arg: string) => arg.startsWith("--qlconfig-file="), ); t.false(hasQlconfigArg, "should NOT have injected a qlconfig"); }); }); const NEW_ANALYSIS_SUMMARY_TEST_CASES = [ { codeqlVersion: makeVersionInfo("2.15.0", { [ToolsFeature.AnalysisSummaryV2IsDefault]: true, }), githubVersion: { type: util.GitHubVariant.DOTCOM, }, flagPassed: false, negativeFlagPassed: false, }, { codeqlVersion: makeVersionInfo("2.15.0"), githubVersion: { type: util.GitHubVariant.DOTCOM, }, flagPassed: true, negativeFlagPassed: false, }, { codeqlVersion: makeVersionInfo("2.15.0"), githubVersion: { type: util.GitHubVariant.GHES, version: "3.10.0", }, flagPassed: true, negativeFlagPassed: false, }, { codeqlVersion: makeVersionInfo("2.14.6"), githubVersion: { type: util.GitHubVariant.DOTCOM, }, flagPassed: false, negativeFlagPassed: false, }, ]; for (const { codeqlVersion, flagPassed, githubVersion, negativeFlagPassed, } of NEW_ANALYSIS_SUMMARY_TEST_CASES) { test(`database interpret-results passes ${ flagPassed ? "--new-analysis-summary" : negativeFlagPassed ? "--no-new-analysis-summary" : "nothing" } for CodeQL version ${JSON.stringify(codeqlVersion)} and ${ util.GitHubVariant[githubVersion.type] } ${githubVersion.version ? ` ${githubVersion.version}` : ""}`, async (t) => { const runnerConstructorStub = stubToolRunnerConstructor(); const codeqlObject = await codeql.getCodeQLForTesting(); sinon.stub(codeqlObject, "getVersion").resolves(codeqlVersion); // safeWhich throws because of the test CodeQL object. sinon.stub(safeWhich, "safeWhich").resolves(""); await codeqlObject.databaseInterpretResults( "", [], "", "", "", "-v", "", Object.assign({}, stubConfig, { gitHubVersion: githubVersion }), createFeatures([]), ); const actualArgs = runnerConstructorStub.firstCall.args[1] as string[]; t.is( actualArgs.includes("--new-analysis-summary"), flagPassed, `--new-analysis-summary should${flagPassed ? "" : "n't"} be passed`, ); t.is( actualArgs.includes("--no-new-analysis-summary"), negativeFlagPassed, `--no-new-analysis-summary should${ negativeFlagPassed ? "" : "n't" } be passed`, ); }); } test("runTool summarizes several fatal errors", async (t) => { const heapError = "A fatal error occurred: Evaluator heap must be at least 384.00 MiB"; const datasetImportError = "A fatal error occurred: Dataset import for /home/runner/work/_temp/codeql_databases/javascript/db-javascript failed with code 2"; const cliStderr = `Running TRAP import for CodeQL database at /home/runner/work/_temp/codeql_databases/javascript...\n` + `${heapError}\n${datasetImportError}.`; stubToolRunnerConstructor(32, cliStderr); const codeqlObject = await codeql.getCodeQLForTesting(); sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6")); // safeWhich throws because of the test CodeQL object. sinon.stub(safeWhich, "safeWhich").resolves(""); await t.throwsAsync( async () => await codeqlObject.finalizeDatabase( "db", "--threads=2", "--ram=2048", false, ), { instanceOf: util.ConfigurationError, message: new RegExp( 'Encountered a fatal error while running \\"codeql-for-testing database finalize --finalize-dataset --threads=2 --ram=2048 db\\"\\. ' + `Exit code was 32 and error was: ${datasetImportError.replaceAll( ".", "\\.", )}\\. Context: ${heapError.replaceAll( ".", "\\.", )}\\. See the logs for more details\\.`, ), }, ); }); test("runTool summarizes autobuilder errors", async (t) => { const stderr = ` [2019-09-18 12:00:00] [autobuild] A non-error message [2019-09-18 12:00:00] Untagged message [2019-09-18 12:00:00] [autobuild] [ERROR] Start of the error message [2019-09-18 12:00:00] [autobuild] An interspersed non-error message [2019-09-18 12:00:01] [autobuild] [ERROR] Some more context about the error message [2019-09-18 12:00:01] [autobuild] [ERROR] continued [2019-09-18 12:00:01] [autobuild] [ERROR] and finished here. [2019-09-18 12:00:01] [autobuild] A non-error message `; stubToolRunnerConstructor(1, stderr); const codeqlObject = await codeql.getCodeQLForTesting(); sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6")); sinon.stub(codeqlObject, "resolveExtractor").resolves("/path/to/extractor"); // safeWhich throws because of the test CodeQL object. sinon.stub(safeWhich, "safeWhich").resolves(""); await t.throwsAsync( async () => await codeqlObject.runAutobuild(stubConfig, Language.java), { instanceOf: util.ConfigurationError, message: "We were unable to automatically build your code. Please provide manual build steps. " + `See ${DocUrl.AUTOMATIC_BUILD_FAILED} for more information. ` + "Encountered the following error: Start of the error message\n" + " Some more context about the error message\n" + " continued\n" + " and finished here.", }, ); }); test("runTool truncates long autobuilder errors", async (t) => { const stderr = Array.from( { length: 20 }, (_, i) => `[2019-09-18 12:00:00] [autobuild] [ERROR] line${i + 1}`, ).join("\n"); stubToolRunnerConstructor(1, stderr); const codeqlObject = await codeql.getCodeQLForTesting(); sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6")); sinon.stub(codeqlObject, "resolveExtractor").resolves("/path/to/extractor"); // safeWhich throws because of the test CodeQL object. sinon.stub(safeWhich, "safeWhich").resolves(""); await t.throwsAsync( async () => await codeqlObject.runAutobuild(stubConfig, Language.java), { instanceOf: util.ConfigurationError, message: "We were unable to automatically build your code. Please provide manual build steps. " + `See ${DocUrl.AUTOMATIC_BUILD_FAILED} for more information. ` + "Encountered the following error: " + `${Array.from({ length: 10 }, (_, i) => `line${i + 1}`).join( "\n", )}\n(truncated)`, }, ); }); test("runTool recognizes fatal internal errors", async (t) => { const stderr = ` [11/31 eval 8m19s] Evaluation done; writing results to codeql/go-queries/Security/CWE-020/MissingRegexpAnchor.bqrs. Oops! A fatal internal error occurred. Details: com.semmle.util.exception.CatastrophicError: An error occurred while evaluating ControlFlowGraph::ControlFlow::Root.isRootOf/1#dispred#f610e6ed/2@86282cc8 Severe disk cache trouble (corruption or out of space) at /home/runner/work/_temp/codeql_databases/go/db-go/default/cache/pages/28/33.pack: Failed to write item to disk`; stubToolRunnerConstructor(1, stderr); const codeqlObject = await codeql.getCodeQLForTesting(); sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6")); sinon.stub(codeqlObject, "resolveExtractor").resolves("/path/to/extractor"); // safeWhich throws because of the test CodeQL object. sinon.stub(safeWhich, "safeWhich").resolves(""); await t.throwsAsync( async () => await codeqlObject.databaseRunQueries(stubConfig.dbLocation, []), { instanceOf: CommandInvocationError, message: `Encountered a fatal error while running "codeql-for-testing database run-queries --expect-discarded-cache --min-disk-free=1024 -v --intra-layer-parallelism". Exit code was 1 and error was: Oops! A fatal internal error occurred. Details: com.semmle.util.exception.CatastrophicError: An error occurred while evaluating ControlFlowGraph::ControlFlow::Root.isRootOf/1#dispred#f610e6ed/2@86282cc8 Severe disk cache trouble (corruption or out of space) at /home/runner/work/_temp/codeql_databases/go/db-go/default/cache/pages/28/33.pack: Failed to write item to disk. See the logs for more details.`, }, ); }); test("runTool outputs last line of stderr if fatal error could not be found", async (t) => { const cliStderr = "line1\nline2\nline3\nline4\nline5"; stubToolRunnerConstructor(32, cliStderr); const codeqlObject = await codeql.getCodeQLForTesting(); sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6")); // safeWhich throws because of the test CodeQL object. sinon.stub(safeWhich, "safeWhich").resolves(""); await t.throwsAsync( async () => await codeqlObject.finalizeDatabase( "db", "--threads=2", "--ram=2048", false, ), { instanceOf: util.ConfigurationError, message: new RegExp( 'Encountered a fatal error while running \\"codeql-for-testing database finalize --finalize-dataset --threads=2 --ram=2048 db\\"\\. ' + "Exit code was 32 and last log line was: line5\\. See the logs for more details\\.", ), }, ); }); test("Avoids duplicating --overwrite flag if specified in CODEQL_ACTION_EXTRA_OPTIONS", async (t) => { const runnerConstructorStub = stubToolRunnerConstructor(); const codeqlObject = await codeql.getCodeQLForTesting(); sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6")); // safeWhich throws because of the test CodeQL object. sinon.stub(safeWhich, "safeWhich").resolves(""); process.env["CODEQL_ACTION_EXTRA_OPTIONS"] = '{ "database": { "init": ["--overwrite"] } }'; await codeqlObject.databaseInitCluster( stubConfig, "sourceRoot", undefined, undefined, getRunnerLogger(false), ); t.true(runnerConstructorStub.calledOnce); const args = runnerConstructorStub.firstCall.args[1] as string[]; t.is( args.filter((option: string) => option === "--overwrite").length, 1, "--overwrite should only be passed once", ); // Clean up const configArg = args.find((arg: string) => arg.startsWith("--codescanning-config="), ); t.truthy(configArg, "Should have injected a codescanning config"); const configFile = configArg!.split("=")[1]; await del(configFile, { force: true }); }); export function stubToolRunnerConstructor( exitCode: number = 0, stderr?: string, ): sinon.SinonStub { const runnerObjectStub = sinon.createStubInstance(toolrunner.ToolRunner); const runnerConstructorStub = sinon.stub( toolrunner, "ToolRunner", ) as sinon.SinonStub; let stderrListener: ((data: Buffer) => void) | undefined = undefined; runnerConstructorStub.callsFake((_cmd, _args, options: ExecOptions) => { stderrListener = options.listeners?.stderr; return runnerObjectStub; }); runnerObjectStub.exec.callsFake(async () => { if (stderrListener !== undefined && stderr !== undefined) { stderrListener(Buffer.from(stderr)); } return exitCode; }); return runnerConstructorStub; }