Files
codeql-action/src/config-utils.test.ts
2025-09-02 17:31:57 +01:00

1753 lines
45 KiB
TypeScript

import * as fs from "fs";
import * as path from "path";
import * as github from "@actions/github";
import test, { ExecutionContext } from "ava";
import * as yaml from "js-yaml";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { AnalysisKind } from "./analyses";
import * as api from "./api-client";
import { CachingKind } from "./caching-utils";
import { createStubCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { Feature } from "./feature-flags";
import * as gitUtils from "./git-utils";
import { KnownLanguage, Language } from "./languages";
import { getRunnerLogger } from "./logging";
import {
CODEQL_OVERLAY_MINIMUM_VERSION,
OverlayDatabaseMode,
} from "./overlay-database-utils";
import { parseRepositoryNwo } from "./repository";
import {
setupTests,
mockLanguagesInRepo as mockLanguagesInRepo,
createFeatures,
getRecordingLogger,
LoggedMessage,
mockCodeQLVersion,
} from "./testing-utils";
import {
GitHubVariant,
GitHubVersion,
prettyPrintPack,
ConfigurationError,
withTmpDir,
BuildMode,
} from "./util";
setupTests(test);
const githubVersion = { type: GitHubVariant.DOTCOM } as GitHubVersion;
function createTestInitConfigInputs(
overrides: Partial<configUtils.InitConfigInputs>,
): configUtils.InitConfigInputs {
return Object.assign(
{},
{
analysisKindsInput: "code-scanning",
languagesInput: undefined,
queriesInput: undefined,
qualityQueriesInput: undefined,
packsInput: undefined,
configFile: undefined,
dbLocation: undefined,
configInput: undefined,
buildModeInput: undefined,
trapCachingEnabled: false,
dependencyCachingEnabled: CachingKind.None,
debugMode: false,
debugArtifactName: "",
debugDatabaseName: "",
repository: { owner: "github", repo: "example" },
tempDir: "",
codeql: createStubCodeQL({
async betterResolveLanguages() {
return {
extractors: {
html: [{ extractor_root: "" }],
javascript: [{ extractor_root: "" }],
},
};
},
}),
workspacePath: "",
sourceRoot: "",
githubVersion,
apiDetails: {
auth: "token",
externalRepoAuth: "token",
url: "https://github.example.com",
apiURL: undefined,
registriesAuthTokens: undefined,
},
features: createFeatures([]),
logger: getRunnerLogger(true),
},
overrides,
);
}
// Returns the filepath of the newly-created file
function createConfigFile(inputFileContents: string, tmpDir: string): string {
const configFilePath = path.join(tmpDir, "input");
fs.writeFileSync(configFilePath, inputFileContents, "utf8");
return configFilePath;
}
type GetContentsResponse = { content?: string } | object[];
function mockGetContents(
content: GetContentsResponse,
): sinon.SinonStub<any, any> {
// Passing an auth token is required, so we just use a dummy value
const client = github.getOctokit("123");
const response = {
data: content,
};
const spyGetContents = sinon
.stub(client.rest.repos, "getContent")
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
.resolves(response as any);
sinon.stub(api, "getApiClient").value(() => client);
sinon.stub(api, "getApiClientWithExternalAuth").value(() => client);
return spyGetContents;
}
function mockListLanguages(languages: string[]) {
// Passing an auth token is required, so we just use a dummy value
const client = github.getOctokit("123");
const response = {
data: {},
};
for (const language of languages) {
response.data[language] = 123;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
sinon.stub(client.rest.repos, "listLanguages").resolves(response as any);
sinon.stub(api, "getApiClient").value(() => client);
}
test("load empty config", async (t) => {
return await withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const languages = "javascript,python";
const codeql = createStubCodeQL({
async betterResolveLanguages() {
return {
extractors: {
javascript: [{ extractor_root: "" }],
python: [{ extractor_root: "" }],
},
};
},
});
const config = await configUtils.initConfig(
createTestInitConfigInputs({
languagesInput: languages,
repository: { owner: "github", repo: "example" },
tempDir,
codeql,
logger,
}),
);
t.deepEqual(
config,
await configUtils.getDefaultConfig(
createTestInitConfigInputs({
languagesInput: languages,
tempDir,
codeql,
logger,
}),
),
);
});
});
test("loading config saves config", async (t) => {
return await withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const codeql = createStubCodeQL({
async betterResolveLanguages() {
return {
extractors: {
javascript: [{ extractor_root: "" }],
python: [{ extractor_root: "" }],
},
};
},
});
// Sanity check the saved config file does not already exist
t.false(fs.existsSync(configUtils.getPathToParsedConfigFile(tempDir)));
// Sanity check that getConfig returns undefined before we have called initConfig
t.deepEqual(await configUtils.getConfig(tempDir, logger), undefined);
const config1 = await configUtils.initConfig(
createTestInitConfigInputs({
languagesInput: "javascript,python",
tempDir,
codeql,
workspacePath: tempDir,
logger,
}),
);
// The saved config file should now exist
t.true(fs.existsSync(configUtils.getPathToParsedConfigFile(tempDir)));
// And that same newly-initialised config should now be returned by getConfig
const config2 = await configUtils.getConfig(tempDir, logger);
t.not(config2, undefined);
if (config2 !== undefined) {
// removes properties assigned to undefined.
const expectedConfig = JSON.parse(JSON.stringify(config1));
t.deepEqual(expectedConfig, config2);
}
});
});
test("load input outside of workspace", async (t) => {
return await withTmpDir(async (tempDir) => {
try {
await configUtils.initConfig(
createTestInitConfigInputs({
configFile: "../input",
tempDir,
workspacePath: tempDir,
}),
);
throw new Error("initConfig did not throw error");
} catch (err) {
t.deepEqual(
err,
new ConfigurationError(
configUtils.getConfigFileOutsideWorkspaceErrorMessage(
path.join(tempDir, "../input"),
),
),
);
}
});
});
test("load non-local input with invalid repo syntax", async (t) => {
return await withTmpDir(async (tempDir) => {
// no filename given, just a repo
const configFile = "octo-org/codeql-config@main";
try {
await configUtils.initConfig(
createTestInitConfigInputs({
configFile,
tempDir,
workspacePath: tempDir,
}),
);
throw new Error("initConfig did not throw error");
} catch (err) {
t.deepEqual(
err,
new ConfigurationError(
configUtils.getConfigFileRepoFormatInvalidMessage(
"octo-org/codeql-config@main",
),
),
);
}
});
});
test("load non-existent input", async (t) => {
return await withTmpDir(async (tempDir) => {
const languagesInput = "javascript";
const configFile = "input";
t.false(fs.existsSync(path.join(tempDir, configFile)));
try {
await configUtils.initConfig(
createTestInitConfigInputs({
languagesInput,
configFile,
tempDir,
workspacePath: tempDir,
}),
);
throw new Error("initConfig did not throw error");
} catch (err) {
t.deepEqual(
err,
new ConfigurationError(
configUtils.getConfigFileDoesNotExistErrorMessage(
path.join(tempDir, "input"),
),
),
);
}
});
});
test("load non-empty input", async (t) => {
return await withTmpDir(async (tempDir) => {
const codeql = createStubCodeQL({
async betterResolveLanguages() {
return {
extractors: {
javascript: [{ extractor_root: "" }],
},
};
},
});
// Just create a generic config object with non-default values for all fields
const inputFileContents = `
name: my config
disable-default-queries: true
queries:
- uses: ./foo
paths-ignore:
- a
- b
paths:
- c/d`;
fs.mkdirSync(path.join(tempDir, "foo"));
// And the config we expect it to parse to
const expectedConfig: configUtils.Config = {
analysisKinds: [AnalysisKind.CodeScanning],
languages: [KnownLanguage.javascript],
buildMode: BuildMode.None,
originalUserInput: {
name: "my config",
"disable-default-queries": true,
queries: [{ uses: "./foo" }],
"paths-ignore": ["a", "b"],
paths: ["c/d"],
},
tempDir,
codeQLCmd: codeql.getPath(),
gitHubVersion: githubVersion,
dbLocation: path.resolve(tempDir, "codeql_databases"),
debugMode: false,
debugArtifactName: "my-artifact",
debugDatabaseName: "my-db",
augmentationProperties: configUtils.defaultAugmentationProperties,
trapCaches: {},
trapCacheDownloadTime: 0,
dependencyCachingEnabled: CachingKind.None,
};
const languagesInput = "javascript";
const configFilePath = createConfigFile(inputFileContents, tempDir);
const actualConfig = await configUtils.initConfig(
createTestInitConfigInputs({
languagesInput,
buildModeInput: "none",
configFile: configFilePath,
debugArtifactName: "my-artifact",
debugDatabaseName: "my-db",
tempDir,
codeql,
workspacePath: tempDir,
}),
);
// Should exactly equal the object we constructed earlier
t.deepEqual(actualConfig, expectedConfig);
});
});
test("Using config input and file together, config input should be used.", async (t) => {
return await withTmpDir(async (tempDir) => {
process.env["RUNNER_TEMP"] = tempDir;
process.env["GITHUB_WORKSPACE"] = tempDir;
const inputFileContents = `
name: my config
queries:
- uses: ./foo_file`;
const configFilePath = createConfigFile(inputFileContents, tempDir);
const configInput = `
name: my config
queries:
- uses: ./foo
packs:
javascript:
- a/b@1.2.3
python:
- c/d@1.2.3
`;
fs.mkdirSync(path.join(tempDir, "foo"));
const codeql = createStubCodeQL({
async betterResolveLanguages() {
return {
extractors: {
javascript: [{ extractor_root: "" }],
python: [{ extractor_root: "" }],
},
};
},
});
// Only JS, python packs will be ignored
const languagesInput = "javascript";
const config = await configUtils.initConfig(
createTestInitConfigInputs({
languagesInput,
configFile: configFilePath,
configInput,
tempDir,
codeql,
workspacePath: tempDir,
}),
);
t.deepEqual(config.originalUserInput, yaml.load(configInput));
});
});
test("API client used when reading remote config", async (t) => {
return await withTmpDir(async (tempDir) => {
const codeql = createStubCodeQL({
async betterResolveLanguages() {
return {
extractors: {
javascript: [{ extractor_root: "" }],
},
};
},
});
const inputFileContents = `
name: my config
disable-default-queries: true
queries:
- uses: ./
- uses: ./foo
- uses: foo/bar@dev
paths-ignore:
- a
- b
paths:
- c/d`;
const dummyResponse = {
content: Buffer.from(inputFileContents).toString("base64"),
};
const spyGetContents = mockGetContents(dummyResponse);
// Create checkout directory for remote queries repository
fs.mkdirSync(path.join(tempDir, "foo/bar/dev"), { recursive: true });
const configFile = "octo-org/codeql-config/config.yaml@main";
const languagesInput = "javascript";
await configUtils.initConfig(
createTestInitConfigInputs({
languagesInput,
configFile,
tempDir,
codeql,
workspacePath: tempDir,
}),
);
t.assert(spyGetContents.called);
});
});
test("Remote config handles the case where a directory is provided", async (t) => {
return await withTmpDir(async (tempDir) => {
const dummyResponse = []; // directories are returned as arrays
mockGetContents(dummyResponse);
const repoReference = "octo-org/codeql-config/config.yaml@main";
try {
await configUtils.initConfig(
createTestInitConfigInputs({
configFile: repoReference,
tempDir,
workspacePath: tempDir,
}),
);
throw new Error("initConfig did not throw error");
} catch (err) {
t.deepEqual(
err,
new ConfigurationError(
configUtils.getConfigFileDirectoryGivenMessage(repoReference),
),
);
}
});
});
test("Invalid format of remote config handled correctly", async (t) => {
return await withTmpDir(async (tempDir) => {
const dummyResponse = {
// note no "content" property here
};
mockGetContents(dummyResponse);
const repoReference = "octo-org/codeql-config/config.yaml@main";
try {
await configUtils.initConfig(
createTestInitConfigInputs({
configFile: repoReference,
tempDir,
workspacePath: tempDir,
}),
);
throw new Error("initConfig did not throw error");
} catch (err) {
t.deepEqual(
err,
new ConfigurationError(
configUtils.getConfigFileFormatInvalidMessage(repoReference),
),
);
}
});
});
test("No detected languages", async (t) => {
return await withTmpDir(async (tempDir) => {
mockListLanguages([]);
const codeql = createStubCodeQL({
async resolveLanguages() {
return {};
},
});
try {
await configUtils.initConfig(
createTestInitConfigInputs({
tempDir,
codeql,
workspacePath: tempDir,
}),
);
throw new Error("initConfig did not throw error");
} catch (err) {
t.deepEqual(
err,
new ConfigurationError(configUtils.getNoLanguagesError()),
);
}
});
});
test("Unknown languages", async (t) => {
return await withTmpDir(async (tempDir) => {
const languagesInput = "rubbish,english";
try {
await configUtils.initConfig(
createTestInitConfigInputs({
languagesInput,
tempDir,
workspacePath: tempDir,
}),
);
throw new Error("initConfig did not throw error");
} catch (err) {
t.deepEqual(
err,
new ConfigurationError(
configUtils.getUnknownLanguagesError(["rubbish", "english"]),
),
);
}
});
});
/**
* Test macro for ensuring the packs block is valid
*/
const parsePacksMacro = test.macro({
exec: (
t: ExecutionContext<unknown>,
packsInput: string,
languages: Language[],
expected: configUtils.Packs | undefined,
) =>
t.deepEqual(
configUtils.parsePacksFromInput(packsInput, languages, false),
expected,
),
title: (providedTitle = "") => `Parse Packs: ${providedTitle}`,
});
/**
* Test macro for testing when the packs block is invalid
*/
const parsePacksErrorMacro = test.macro({
exec: (
t: ExecutionContext<unknown>,
packsInput: string,
languages: Language[],
expected: RegExp,
) =>
t.throws(
() => configUtils.parsePacksFromInput(packsInput, languages, false),
{
message: expected,
},
),
title: (providedTitle = "") => `Parse Packs Error: ${providedTitle}`,
});
/**
* Test macro for testing when the packs block is invalid
*/
const invalidPackNameMacro = test.macro({
exec: (t: ExecutionContext, name: string) =>
parsePacksErrorMacro.exec(
t,
name,
[KnownLanguage.cpp],
new RegExp(`^"${name}" is not a valid pack$`),
),
title: (_providedTitle: string | undefined, arg: string | undefined) =>
`Invalid pack string: ${arg}`,
});
test("no packs", parsePacksMacro, "", [], undefined);
test("two packs", parsePacksMacro, "a/b,c/d@1.2.3", [KnownLanguage.cpp], {
[KnownLanguage.cpp]: ["a/b", "c/d@1.2.3"],
});
test(
"two packs with spaces",
parsePacksMacro,
" a/b , c/d@1.2.3 ",
[KnownLanguage.cpp],
{
[KnownLanguage.cpp]: ["a/b", "c/d@1.2.3"],
},
);
test(
"two packs with language",
parsePacksErrorMacro,
"a/b,c/d@1.2.3",
[KnownLanguage.cpp, KnownLanguage.java],
new RegExp(
"Cannot specify a 'packs' input in a multi-language analysis. " +
"Use a codeql-config.yml file instead and specify packs by language.",
),
);
test(
"packs with other valid names",
parsePacksMacro,
[
// ranges are ok
"c/d@1.0",
"c/d@~1.0.0",
"c/d@~1.0.0:a/b",
"c/d@~1.0.0+abc:a/b",
"c/d@~1.0.0-abc:a/b",
"c/d:a/b",
// whitespace is removed
" c/d @ ~1.0.0 : b.qls ",
// and it is retained within a path
" c/d @ ~1.0.0 : b/a path with/spaces.qls ",
// this is valid. the path is '@'. It will probably fail when passed to the CLI
"c/d@1.2.3:@",
// this is valid, too. It will fail if it doesn't match a path
// (globbing is not done)
"c/d@1.2.3:+*)_(",
].join(","),
[KnownLanguage.cpp],
{
[KnownLanguage.cpp]: [
"c/d@1.0",
"c/d@~1.0.0",
"c/d@~1.0.0:a/b",
"c/d@~1.0.0+abc:a/b",
"c/d@~1.0.0-abc:a/b",
"c/d:a/b",
"c/d@~1.0.0:b.qls",
"c/d@~1.0.0:b/a path with/spaces.qls",
"c/d@1.2.3:@",
"c/d@1.2.3:+*)_(",
],
},
);
test(invalidPackNameMacro, "c"); // all packs require at least a scope and a name
test(invalidPackNameMacro, "c-/d");
test(invalidPackNameMacro, "-c/d");
test(invalidPackNameMacro, "c/d_d");
test(invalidPackNameMacro, "c/d@@");
test(invalidPackNameMacro, "c/d@1.0.0:");
test(invalidPackNameMacro, "c/d:");
test(invalidPackNameMacro, "c/d:/a");
test(invalidPackNameMacro, "@1.0.0:a");
test(invalidPackNameMacro, "c/d@../a");
test(invalidPackNameMacro, "c/d@b/../a");
test(invalidPackNameMacro, "c/d:z@1");
/**
* Test macro for pretty printing pack specs
*/
const packSpecPrettyPrintingMacro = test.macro({
exec: (t: ExecutionContext, packStr: string, packObj: configUtils.Pack) => {
const parsed = configUtils.parsePacksSpecification(packStr);
t.deepEqual(parsed, packObj, "parsed pack spec is correct");
const stringified = prettyPrintPack(packObj);
t.deepEqual(
stringified,
packStr.trim(),
"pretty-printed pack spec is correct",
);
t.deepEqual(
configUtils.validatePackSpecification(packStr),
packStr.trim(),
"pack spec is valid",
);
},
title: (
_providedTitle: string | undefined,
packStr: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_packObj: configUtils.Pack,
) => `Prettyprint pack spec: '${packStr}'`,
});
test(packSpecPrettyPrintingMacro, "a/b", {
name: "a/b",
version: undefined,
path: undefined,
});
test(packSpecPrettyPrintingMacro, "a/b@~1.2.3", {
name: "a/b",
version: "~1.2.3",
path: undefined,
});
test(packSpecPrettyPrintingMacro, "a/b@~1.2.3:abc/def", {
name: "a/b",
version: "~1.2.3",
path: "abc/def",
});
test(packSpecPrettyPrintingMacro, "a/b:abc/def", {
name: "a/b",
version: undefined,
path: "abc/def",
});
test(packSpecPrettyPrintingMacro, " a/b:abc/def ", {
name: "a/b",
version: undefined,
path: "abc/def",
});
const mockLogger = getRunnerLogger(true);
const calculateAugmentationMacro = test.macro({
exec: async (
t: ExecutionContext,
_title: string,
rawPacksInput: string | undefined,
rawQueriesInput: string | undefined,
languages: Language[],
expectedAugmentationProperties: configUtils.AugmentationProperties,
) => {
const actualAugmentationProperties =
await configUtils.calculateAugmentation(
rawPacksInput,
rawQueriesInput,
languages,
);
t.deepEqual(actualAugmentationProperties, expectedAugmentationProperties);
},
title: (_, title) => `Calculate Augmentation: ${title}`,
});
test(
calculateAugmentationMacro,
"All empty",
undefined,
undefined,
[KnownLanguage.javascript],
{
...configUtils.defaultAugmentationProperties,
},
);
test(
calculateAugmentationMacro,
"With queries",
undefined,
" a, b , c, d",
[KnownLanguage.javascript],
{
...configUtils.defaultAugmentationProperties,
queriesInput: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
},
);
test(
calculateAugmentationMacro,
"With queries combining",
undefined,
" + a, b , c, d ",
[KnownLanguage.javascript],
{
...configUtils.defaultAugmentationProperties,
queriesInputCombines: true,
queriesInput: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
},
);
test(
calculateAugmentationMacro,
"With packs",
" codeql/a , codeql/b , codeql/c , codeql/d ",
undefined,
[KnownLanguage.javascript],
{
...configUtils.defaultAugmentationProperties,
packsInput: ["codeql/a", "codeql/b", "codeql/c", "codeql/d"],
},
);
test(
calculateAugmentationMacro,
"With packs combining",
" + codeql/a, codeql/b, codeql/c, codeql/d",
undefined,
[KnownLanguage.javascript],
{
...configUtils.defaultAugmentationProperties,
packsInputCombines: true,
packsInput: ["codeql/a", "codeql/b", "codeql/c", "codeql/d"],
},
);
const calculateAugmentationErrorMacro = test.macro({
exec: async (
t: ExecutionContext,
_title: string,
rawPacksInput: string | undefined,
rawQueriesInput: string | undefined,
languages: Language[],
expectedError: RegExp | string,
) => {
await t.throwsAsync(
() =>
configUtils.calculateAugmentation(
rawPacksInput,
rawQueriesInput,
languages,
),
{ message: expectedError },
);
},
title: (_, title) => `Calculate Augmentation Error: ${title}`,
});
test(
calculateAugmentationErrorMacro,
"Plus (+) with nothing else (queries)",
undefined,
" + ",
[KnownLanguage.javascript],
/The workflow property "queries" is invalid/,
);
test(
calculateAugmentationErrorMacro,
"Plus (+) with nothing else (packs)",
" + ",
undefined,
[KnownLanguage.javascript],
/The workflow property "packs" is invalid/,
);
test(
calculateAugmentationErrorMacro,
"Packs input with multiple languages",
" + a/b, c/d ",
undefined,
[KnownLanguage.javascript, KnownLanguage.java],
/Cannot specify a 'packs' input in a multi-language analysis/,
);
test(
calculateAugmentationErrorMacro,
"Packs input with no languages",
" + a/b, c/d ",
undefined,
[],
/No languages specified/,
);
test(
calculateAugmentationErrorMacro,
"Invalid packs",
" a-pack-without-a-scope ",
undefined,
[KnownLanguage.javascript],
/"a-pack-without-a-scope" is not a valid pack/,
);
test("no generateRegistries when registries is undefined", async (t) => {
return await withTmpDir(async (tmpDir) => {
const registriesInput = undefined;
const logger = getRunnerLogger(true);
const { registriesAuthTokens, qlconfigFile } =
await configUtils.generateRegistries(registriesInput, tmpDir, logger);
t.is(registriesAuthTokens, undefined);
t.is(qlconfigFile, undefined);
});
});
test("generateRegistries prefers original CODEQL_REGISTRIES_AUTH", async (t) => {
return await withTmpDir(async (tmpDir) => {
process.env.CODEQL_REGISTRIES_AUTH = "original";
const registriesInput = yaml.dump([
{
url: "http://ghcr.io",
packages: ["codeql/*", "codeql-testing/*"],
token: "not-a-token",
},
]);
const logger = getRunnerLogger(true);
const { registriesAuthTokens, qlconfigFile } =
await configUtils.generateRegistries(registriesInput, tmpDir, logger);
t.is(registriesAuthTokens, "original");
t.is(qlconfigFile, path.join(tmpDir, "qlconfig.yml"));
});
});
// getLanguages
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
// eslint-disable-next-line github/array-foreach
[
{
name: "languages from input",
languagesInput: "jAvAscript, \n jaVa",
languagesInRepository: ["SwiFt", "other"],
expectedLanguages: ["javascript", "java"],
expectedApiCall: false,
},
{
name: "languages from github api",
languagesInput: "",
languagesInRepository: [" jAvAscript\n \t", " jaVa", "SwiFt", "other"],
expectedLanguages: ["javascript", "java"],
expectedApiCall: true,
},
{
name: "aliases from input",
languagesInput: " typEscript\n \t, C#, c , KoTlin",
languagesInRepository: ["SwiFt", "other"],
expectedLanguages: ["javascript", "csharp", "cpp", "java"],
expectedApiCall: false,
},
{
name: "duplicate languages from input",
languagesInput: "jAvAscript, \n jaVa, kotlin, typescript",
languagesInRepository: ["SwiFt", "other"],
expectedLanguages: ["javascript", "java"],
expectedApiCall: false,
},
{
name: "aliases from github api",
languagesInput: "",
languagesInRepository: [" typEscript\n \t", " C#", "c", "other"],
expectedLanguages: ["javascript", "csharp", "cpp"],
expectedApiCall: true,
},
{
name: "no languages",
languagesInput: "",
languagesInRepository: [],
expectedApiCall: true,
expectedError: configUtils.getNoLanguagesError(),
},
{
name: "unrecognized languages from input",
languagesInput: "a, b, c, javascript",
languagesInRepository: [],
expectedApiCall: false,
expectedError: configUtils.getUnknownLanguagesError(["a", "b"]),
},
{
name: "extractors that aren't languages aren't included (specified)",
languagesInput: "html",
languagesInRepository: [],
expectedApiCall: false,
expectedError: configUtils.getUnknownLanguagesError(["html"]),
},
{
name: "extractors that aren't languages aren't included (autodetected)",
languagesInput: "",
languagesInRepository: ["html", "javascript"],
expectedApiCall: true,
expectedLanguages: ["javascript"],
},
].forEach((args) => {
test(`getLanguages: ${args.name}`, async (t) => {
const mockRequest = mockLanguagesInRepo(args.languagesInRepository);
const stubExtractorEntry = {
extractor_root: "",
};
const codeQL = createStubCodeQL({
betterResolveLanguages: () =>
Promise.resolve({
aliases: {
"c#": KnownLanguage.csharp,
c: KnownLanguage.cpp,
kotlin: KnownLanguage.java,
typescript: KnownLanguage.javascript,
},
extractors: {
cpp: [stubExtractorEntry],
csharp: [stubExtractorEntry],
java: [stubExtractorEntry],
javascript: [stubExtractorEntry],
python: [stubExtractorEntry],
},
}),
});
if (args.expectedLanguages) {
// happy path
const actualLanguages = await configUtils.getLanguages(
codeQL,
args.languagesInput,
mockRepositoryNwo,
".",
mockLogger,
);
t.deepEqual(actualLanguages.sort(), args.expectedLanguages.sort());
} else {
// there is an error
await t.throwsAsync(
async () =>
await configUtils.getLanguages(
codeQL,
args.languagesInput,
mockRepositoryNwo,
".",
mockLogger,
),
{ message: args.expectedError },
);
}
t.deepEqual(mockRequest.called, args.expectedApiCall);
});
});
for (const { displayName, language, feature } of [
{
displayName: "Java",
language: KnownLanguage.java,
feature: Feature.DisableJavaBuildlessEnabled,
},
{
displayName: "C#",
language: KnownLanguage.csharp,
feature: Feature.DisableCsharpBuildless,
},
]) {
test(`Build mode not overridden when disable ${displayName} buildless feature flag disabled`, async (t) => {
const messages: LoggedMessage[] = [];
const buildMode = await configUtils.parseBuildModeInput(
"none",
[language],
createFeatures([]),
getRecordingLogger(messages),
);
t.is(buildMode, BuildMode.None);
t.deepEqual(messages, []);
});
test(`Build mode not overridden for other languages when disable ${displayName} buildless feature flag enabled`, async (t) => {
const messages: LoggedMessage[] = [];
const buildMode = await configUtils.parseBuildModeInput(
"none",
[KnownLanguage.python],
createFeatures([feature]),
getRecordingLogger(messages),
);
t.is(buildMode, BuildMode.None);
t.deepEqual(messages, []);
});
test(`Build mode overridden when analyzing ${displayName} and disable ${displayName} buildless feature flag enabled`, async (t) => {
const messages: LoggedMessage[] = [];
const buildMode = await configUtils.parseBuildModeInput(
"none",
[language],
createFeatures([feature]),
getRecordingLogger(messages),
);
t.is(buildMode, BuildMode.Autobuild);
t.deepEqual(messages, [
{
message: `Scanning ${displayName} code without a build is temporarily unavailable. Falling back to 'autobuild' build mode.`,
type: "warning",
},
]);
});
}
interface OverlayDatabaseModeTestSetup {
overlayDatabaseEnvVar: string | undefined;
features: Feature[];
isPullRequest: boolean;
isDefaultBranch: boolean;
repositoryOwner: string;
buildMode: BuildMode | undefined;
languages: Language[];
codeqlVersion: string;
gitRoot: string | undefined;
codeScanningConfig: configUtils.UserConfig;
}
const defaultOverlayDatabaseModeTestSetup: OverlayDatabaseModeTestSetup = {
overlayDatabaseEnvVar: undefined,
features: [],
isPullRequest: false,
isDefaultBranch: false,
repositoryOwner: "github",
buildMode: BuildMode.None,
languages: [KnownLanguage.javascript],
codeqlVersion: CODEQL_OVERLAY_MINIMUM_VERSION,
gitRoot: "/some/git/root",
codeScanningConfig: {},
};
const getOverlayDatabaseModeMacro = test.macro({
exec: async (
t: ExecutionContext,
_title: string,
setupOverrides: Partial<OverlayDatabaseModeTestSetup>,
expected: {
overlayDatabaseMode: OverlayDatabaseMode;
useOverlayDatabaseCaching: boolean;
},
) => {
return await withTmpDir(async (tempDir) => {
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
// Save the original environment
const originalEnv = { ...process.env };
try {
const setup = {
...defaultOverlayDatabaseModeTestSetup,
...setupOverrides,
};
// Set up environment variable if specified
delete process.env.CODEQL_OVERLAY_DATABASE_MODE;
if (setup.overlayDatabaseEnvVar !== undefined) {
process.env.CODEQL_OVERLAY_DATABASE_MODE =
setup.overlayDatabaseEnvVar;
}
// Mock feature flags
const features = createFeatures(setup.features);
// Mock isAnalyzingPullRequest function
sinon
.stub(actionsUtil, "isAnalyzingPullRequest")
.returns(setup.isPullRequest);
// Mock repository owner
const repository = {
owner: setup.repositoryOwner,
repo: "test-repo",
};
// Set up CodeQL mock
const codeql = mockCodeQLVersion(setup.codeqlVersion);
// Mock traced languages
sinon
.stub(codeql, "isTracedLanguage")
.callsFake(async (lang: Language) => {
return [KnownLanguage.java].includes(lang as KnownLanguage);
});
// Mock git root detection
if (setup.gitRoot !== undefined) {
sinon.stub(gitUtils, "getGitRoot").resolves(setup.gitRoot);
}
// Mock default branch detection
sinon
.stub(gitUtils, "isAnalyzingDefaultBranch")
.resolves(setup.isDefaultBranch);
const result = await configUtils.getOverlayDatabaseMode(
codeql,
repository,
features,
setup.languages,
tempDir, // sourceRoot
setup.buildMode,
setup.codeScanningConfig,
logger,
);
t.deepEqual(result, expected);
} finally {
// Restore the original environment
process.env = originalEnv;
}
});
},
title: (_, title) => `getOverlayDatabaseMode: ${title}`,
});
test(
getOverlayDatabaseModeMacro,
"Environment variable override - Overlay",
{
overlayDatabaseEnvVar: "overlay",
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Environment variable override - OverlayBase",
{
overlayDatabaseEnvVar: "overlay-base",
},
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Environment variable override - None",
{
overlayDatabaseEnvVar: "none",
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Ignore invalid environment variable",
{
overlayDatabaseEnvVar: "invalid-mode",
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Ignore feature flag when analyzing non-default branch",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay-base database on default branch when feature enabled",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay-base database on default branch when feature enabled with custom analysis",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
codeScanningConfig: {
packs: ["some-custom-pack@1.0.0"],
} as configUtils.UserConfig,
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay-base database on default branch when code-scanning feature enabled",
{
languages: [KnownLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when code-scanning feature enabled with disable-default-queries",
{
languages: [KnownLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
"disable-default-queries": true,
} as configUtils.UserConfig,
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when code-scanning feature enabled with packs",
{
languages: [KnownLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
packs: ["some-custom-pack@1.0.0"],
} as configUtils.UserConfig,
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when code-scanning feature enabled with queries",
{
languages: [KnownLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
queries: [{ uses: "some-query.ql" }],
} as configUtils.UserConfig,
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when code-scanning feature enabled with query-filters",
{
languages: [KnownLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
"query-filters": [{ include: { "security-severity": "high" } }],
} as configUtils.UserConfig,
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when only language-specific feature enabled",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysisJavascript],
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when only code-scanning feature enabled",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysisCodeScanningJavascript],
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when language-specific feature disabled",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysis],
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay analysis on PR when feature enabled",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay analysis on PR when feature enabled with custom analysis",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
codeScanningConfig: {
packs: ["some-custom-pack@1.0.0"],
} as configUtils.UserConfig,
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay analysis on PR when code-scanning feature enabled",
{
languages: [KnownLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when code-scanning feature enabled with disable-default-queries",
{
languages: [KnownLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
"disable-default-queries": true,
} as configUtils.UserConfig,
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when code-scanning feature enabled with packs",
{
languages: [KnownLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
packs: ["some-custom-pack@1.0.0"],
} as configUtils.UserConfig,
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when code-scanning feature enabled with queries",
{
languages: [KnownLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
queries: [{ uses: "some-query.ql" }],
} as configUtils.UserConfig,
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when code-scanning feature enabled with query-filters",
{
languages: [KnownLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
"query-filters": [{ include: { "security-severity": "high" } }],
} as configUtils.UserConfig,
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when only language-specific feature enabled",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysisJavascript],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when only code-scanning feature enabled",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysisCodeScanningJavascript],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when language-specific feature disabled",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysis],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay PR analysis by env for dsp-testing",
{
overlayDatabaseEnvVar: "overlay",
repositoryOwner: "dsp-testing",
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay PR analysis by env for other-org",
{
overlayDatabaseEnvVar: "overlay",
repositoryOwner: "other-org",
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay PR analysis by feature flag for dsp-testing",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isPullRequest: true,
repositoryOwner: "dsp-testing",
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay PR analysis by feature flag for other-org",
{
languages: [KnownLanguage.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isPullRequest: true,
repositoryOwner: "other-org",
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Fallback due to autobuild with traced language",
{
overlayDatabaseEnvVar: "overlay",
buildMode: BuildMode.Autobuild,
languages: [KnownLanguage.java],
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Fallback due to no build mode with traced language",
{
overlayDatabaseEnvVar: "overlay",
buildMode: undefined,
languages: [KnownLanguage.java],
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Fallback due to old CodeQL version",
{
overlayDatabaseEnvVar: "overlay",
codeqlVersion: "2.14.0",
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Fallback due to missing git root",
{
overlayDatabaseEnvVar: "overlay",
gitRoot: undefined,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
// Exercise language-specific overlay analysis features code paths
for (const language in KnownLanguage) {
test(
getOverlayDatabaseModeMacro,
`Check default overlay analysis feature for ${language}`,
{
languages: [language],
features: [Feature.OverlayAnalysis],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
}