diff --git a/examples/hello-world/services/hello-container/garden.yml b/examples/hello-world/services/hello-container/garden.yml index 8e6f8aa0d0..6de9abca8d 100644 --- a/examples/hello-world/services/hello-container/garden.yml +++ b/examples/hello-world/services/hello-container/garden.yml @@ -33,4 +33,4 @@ module: env: FUNCTION_ENDPOINT: ${services.hello-function.outputs.endpoint} dependencies: - - hello-function + - hello-function \ No newline at end of file diff --git a/garden-service/package-lock.json b/garden-service/package-lock.json index 7d92ff9175..1b18358155 100644 --- a/garden-service/package-lock.json +++ b/garden-service/package-lock.json @@ -3750,8 +3750,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "optional": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "aproba": { "version": "1.2.0", @@ -3772,14 +3771,12 @@ "balanced-match": { "version": "1.0.0", "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "optional": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3794,20 +3791,17 @@ "code-point-at": { "version": "1.1.0", "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "optional": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "concat-map": { "version": "0.0.1", "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "optional": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "optional": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "core-util-is": { "version": "1.0.2", @@ -3924,8 +3918,7 @@ "inherits": { "version": "2.0.3", "resolved": false, - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "optional": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -3937,7 +3930,6 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3952,7 +3944,6 @@ "version": "3.0.4", "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3960,14 +3951,12 @@ "minimist": { "version": "0.0.8", "resolved": false, - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "optional": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minipass": { "version": "2.2.4", "resolved": false, "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3986,7 +3975,6 @@ "version": "0.5.1", "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4067,8 +4055,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "optional": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", @@ -4080,7 +4067,6 @@ "version": "1.4.0", "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "optional": true, "requires": { "wrappy": "1" } @@ -4166,8 +4152,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": false, - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "optional": true + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "safer-buffer": { "version": "2.1.2", @@ -4203,7 +4188,6 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4223,7 +4207,6 @@ "version": "3.0.1", "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4267,14 +4250,12 @@ "wrappy": { "version": "1.0.2", "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "optional": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "yallist": { "version": "3.0.2", "resolved": false, - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "optional": true + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" } } }, @@ -7431,7 +7412,6 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -7801,8 +7781,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, - "optional": true + "dev": true }, "is-builtin-module": { "version": "1.0.0", @@ -7898,7 +7877,6 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, - "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -7951,8 +7929,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true, - "optional": true + "dev": true }, "lru-cache": { "version": "4.1.3", @@ -8255,8 +8232,7 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true, - "optional": true + "dev": true }, "require-directory": { "version": "2.1.1", diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index 134b73de24..2a1d998e6f 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -96,7 +96,7 @@ import { } from "./config/base" import { BaseTask } from "./tasks/base" import { LocalConfigStore } from "./config-store" -import { detectCircularDependencies } from "./util/detectCycles" +import { validateDependencies } from "./util/validate-dependencies" import { getLinkedSources, ExternalSourceType, @@ -837,14 +837,17 @@ export class Garden { const moduleConfigContext = new ModuleConfigContext( this, this.log, this.environment, Object.values(this.moduleConfigs), ) - this.moduleConfigs = await resolveTemplateStrings(this.moduleConfigs, moduleConfigContext) - await this.detectCircularDependencies() + this.moduleConfigs = await resolveTemplateStrings(this.moduleConfigs, moduleConfigContext) + this.validateDependencies() }) } - private async detectCircularDependencies() { - return detectCircularDependencies(Object.values(this.moduleConfigs)) + private validateDependencies() { + validateDependencies( + Object.values(this.moduleConfigs), + Object.keys(this.serviceNameIndex), + Object.keys(this.taskNameIndex)) } /* @@ -937,7 +940,7 @@ export class Garden { if (this.modulesScanned) { // need to re-run this if adding modules after initial scan - await this.detectCircularDependencies() + await this.validateDependencies() } } diff --git a/garden-service/src/util/detectCycles.ts b/garden-service/src/util/validate-dependencies.ts similarity index 56% rename from garden-service/src/util/detectCycles.ts rename to garden-service/src/util/validate-dependencies.ts index 261c865de4..6cf8ae1ca0 100644 --- a/garden-service/src/util/detectCycles.ts +++ b/garden-service/src/util/validate-dependencies.ts @@ -7,33 +7,115 @@ */ import dedent = require("dedent") +import { merge } from "lodash" +import * as indentString from "indent-string" import { get, isEqual, join, set, uniqWith } from "lodash" import { getModuleKey } from "../types/module" import { ConfigurationError } from "../exceptions" import { ServiceConfig } from "../config/service" import { TaskConfig } from "../config/task" import { ModuleConfig } from "../config/module" +import { deline } from "./string" -export type Cycle = string[] +export function validateDependencies( + moduleConfigs: ModuleConfig[], serviceNames: string[], taskNames: string[], +): void { -/* - Implements a variation on the Floyd-Warshall algorithm to compute minimal cycles. + const missingDepsError = detectMissingDependencies(moduleConfigs, serviceNames, taskNames) + const circularDepsError = detectCircularDependencies(moduleConfigs) + + let errMsg = "" + let detail = {} + + if (missingDepsError) { + errMsg += missingDepsError.message + detail = merge(detail, missingDepsError.detail) + } + + if (circularDepsError) { + errMsg += "\n" + circularDepsError.message + detail = merge(detail, circularDepsError.detail) + } + + if (missingDepsError || circularDepsError) { + throw new ConfigurationError(errMsg, detail) + } - This is approximately O(m^3) + O(s^3), where m is the number of modules and s is the number of services. +} + +/** + * Looks for dependencies on non-existent modules, services or tasks, and returns an error + * if any were found. + */ +export function detectMissingDependencies( + moduleConfigs: ModuleConfig[], serviceNames: string[], taskNames: string[], +): ConfigurationError | null { + + const moduleNames: Set = new Set(moduleConfigs.map(m => m.name)) + const runtimeNames: Set = new Set([...serviceNames, ...taskNames]) + const missingDepDescriptions: string[] = [] + + const runtimeDepTypes = [ + ["serviceConfigs", "Service"], + ["taskConfigs", "Task"], + ["testConfigs", "Test"], + ] + + for (const m of moduleConfigs) { + + const buildDepKeys = m.build.dependencies.map(d => getModuleKey(d.name, d.plugin)) + + for (const missingModule of buildDepKeys.filter(k => !moduleNames.has(k))) { + missingDepDescriptions.push( + `Module '${m.name}': Unknown module '${missingModule}' referenced in build dependencies.`, + ) + } - Throws an error if cycles were found. -*/ -export async function detectCircularDependencies(moduleConfigs: ModuleConfig[]) { + for (const [configKey, entityName] of runtimeDepTypes) { + for (const config of m[configKey]) { + for (const missingRuntimeDep of config.dependencies.filter(d => !runtimeNames.has(d))) { + missingDepDescriptions.push(deline` + ${entityName} '${config.name}' (in module '${m.name}'): Unknown service or task '${missingRuntimeDep}' + referenced in dependencies.`, + ) + } + } + } + + } + + if (missingDepDescriptions.length > 0) { + const errMsg = "Unknown dependencies detected.\n\n" + + indentString(missingDepDescriptions.join("\n\n"), 2) + "\n" + + return new ConfigurationError(errMsg, { "unknown-dependencies": missingDepDescriptions }) + } else { + return null + } + +} + +export type Cycle = string[] + +/** + * Implements a variation on the Floyd-Warshall algorithm to compute minimal cycles. + * + * This is approximately O(m^3) + O(s^3), where m is the number of modules and s is the number of services. + * + * Returns an error if cycles were found. + */ +export function detectCircularDependencies(moduleConfigs: ModuleConfig[]): ConfigurationError | null { // Sparse matrices const buildGraph = {} const runtimeGraph = {} const services: ServiceConfig[] = [] const tasks: TaskConfig[] = [] - /* - There's no need to account for test dependencies here, since any circularities there - are accounted for via service dependencies. - */ + /** + * Since dependencies listed in test configs cannot introduce circularities (because + * builds/deployments/tasks/tests cannot currently depend on tests), we don't need to + * account for test dependencies here. + */ for (const module of moduleConfigs) { // Build dependencies for (const buildDep of module.build.dependencies) { @@ -83,8 +165,10 @@ export async function detectCircularDependencies(moduleConfigs: ModuleConfig[]) detail["circular-service-or-task-dependencies"] = runtimeCyclesDescription } - throw new ConfigurationError(errMsg, detail) + return new ConfigurationError(errMsg, detail) } + + return null } export function detectCycles(graph, vertices: string[]): Cycle[] { diff --git a/garden-service/test/data/test-project-circular-deps/module-a/garden.yml b/garden-service/test/data/test-project-circular-deps/module-a/garden.yml index 83d952c527..5ea1d0fce1 100644 --- a/garden-service/test/data/test-project-circular-deps/module-a/garden.yml +++ b/garden-service/test/data/test-project-circular-deps/module-a/garden.yml @@ -1,11 +1,8 @@ module: name: module-a - type: exec + type: test services: - name: service-a - ingresses: - - path: /path-a - containerPort: 8080 dependencies: - service-c build: diff --git a/garden-service/test/data/test-project-circular-deps/module-b/garden.yml b/garden-service/test/data/test-project-circular-deps/module-b/garden.yml index 98a211fc56..9fcdd72efb 100644 --- a/garden-service/test/data/test-project-circular-deps/module-b/garden.yml +++ b/garden-service/test/data/test-project-circular-deps/module-b/garden.yml @@ -1,11 +1,8 @@ module: name: module-b - type: exec + type: test services: - name: service-b - ingresses: - - path: /path-b - containerPort: 8080 dependencies: - service-a - service-c diff --git a/garden-service/test/data/test-project-circular-deps/module-c/garden.yml b/garden-service/test/data/test-project-circular-deps/module-c/garden.yml index 4b0b93718d..1b3f3a913d 100644 --- a/garden-service/test/data/test-project-circular-deps/module-c/garden.yml +++ b/garden-service/test/data/test-project-circular-deps/module-c/garden.yml @@ -1,12 +1,9 @@ module: name: module-c - type: exec + type: test allowPublish: false services: - name: service-c - ingresses: - - path: /path-c - containerPort: 8080 dependencies: - service-b build: diff --git a/garden-service/test/data/test-projects/missing-deps/missing-build-dep/garden.yml b/garden-service/test/data/test-projects/missing-deps/missing-build-dep/garden.yml new file mode 100644 index 0000000000..bd1f17723f --- /dev/null +++ b/garden-service/test/data/test-projects/missing-deps/missing-build-dep/garden.yml @@ -0,0 +1,11 @@ +project: + name: test-project-a + environmentDefaults: + variables: + some: variable + environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other diff --git a/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-a/.garden-version b/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-a/.garden-version new file mode 100644 index 0000000000..1d754b7141 --- /dev/null +++ b/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-a/.garden-version @@ -0,0 +1,4 @@ +{ + "latestCommit": "1234567890", + "dirtyTimestamp": null +} diff --git a/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-a/garden.yml b/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-a/garden.yml new file mode 100644 index 0000000000..3686a0fb00 --- /dev/null +++ b/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-a/garden.yml @@ -0,0 +1,15 @@ +module: + name: module-a + type: test + services: + - name: service-a + build: + command: [echo, A] + dependencies: + - missing-build-dep + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-a + command: [echo, OK] diff --git a/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-b/garden.yml b/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-b/garden.yml new file mode 100644 index 0000000000..6bd8f70222 --- /dev/null +++ b/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-b/garden.yml @@ -0,0 +1,17 @@ +module: + name: module-b + type: test + services: + - name: service-b + dependencies: + - service-a + build: + command: [echo, B] + dependencies: + - module-a + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-b + command: [echo, OK] diff --git a/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-c/garden.yml b/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-c/garden.yml new file mode 100644 index 0000000000..84ffcaaf3e --- /dev/null +++ b/garden-service/test/data/test-projects/missing-deps/missing-build-dep/module-c/garden.yml @@ -0,0 +1,14 @@ +module: + name: module-c + type: test + services: + - name: service-c + build: + dependencies: + - module-b + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-c + command: [echo, OK] diff --git a/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/garden.yml b/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/garden.yml new file mode 100644 index 0000000000..bd1f17723f --- /dev/null +++ b/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/garden.yml @@ -0,0 +1,11 @@ +project: + name: test-project-a + environmentDefaults: + variables: + some: variable + environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other diff --git a/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-a/.garden-version b/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-a/.garden-version new file mode 100644 index 0000000000..1d754b7141 --- /dev/null +++ b/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-a/.garden-version @@ -0,0 +1,4 @@ +{ + "latestCommit": "1234567890", + "dirtyTimestamp": null +} diff --git a/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-a/garden.yml b/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-a/garden.yml new file mode 100644 index 0000000000..9d287b14cb --- /dev/null +++ b/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-a/garden.yml @@ -0,0 +1,15 @@ +module: + name: module-a + type: test + services: + - name: service-a + dependencies: + - missing-runtime-dep + build: + command: [echo, A] + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-a + command: [echo, OK] diff --git a/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-b/garden.yml b/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-b/garden.yml new file mode 100644 index 0000000000..6bd8f70222 --- /dev/null +++ b/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-b/garden.yml @@ -0,0 +1,17 @@ +module: + name: module-b + type: test + services: + - name: service-b + dependencies: + - service-a + build: + command: [echo, B] + dependencies: + - module-a + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-b + command: [echo, OK] diff --git a/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-c/garden.yml b/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-c/garden.yml new file mode 100644 index 0000000000..84ffcaaf3e --- /dev/null +++ b/garden-service/test/data/test-projects/missing-deps/missing-runtime-dep/module-c/garden.yml @@ -0,0 +1,14 @@ +module: + name: module-c + type: test + services: + - name: service-c + build: + dependencies: + - module-b + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-c + command: [echo, OK] diff --git a/garden-service/test/src/garden.ts b/garden-service/test/src/garden.ts index 7be090c20a..7a2864cdb5 100644 --- a/garden-service/test/src/garden.ts +++ b/garden-service/test/src/garden.ts @@ -2,7 +2,6 @@ import { expect } from "chai" import * as td from "testdouble" import { join, resolve } from "path" import { Garden } from "../../src/garden" -import { detectCycles } from "../../src/util/detectCycles" import { dataDir, expectError, @@ -330,41 +329,6 @@ describe("Garden", () => { expect(getNames(modules).sort()).to.eql(["module-a", "module-b", "module-c"]) }) - describe("detectCircularDependencies", () => { - it("should throw an exception when circular dependencies are present", async () => { - const circularProjectRoot = join(__dirname, "..", "data", "test-project-circular-deps") - const garden = await makeTestGarden(circularProjectRoot) - await expectError( - async () => await garden.scanModules(), - "configuration") - }) - - it("should not throw an exception when no circular dependencies are present", async () => { - const nonCircularProjectRoot = join(__dirname, "..", "data", "test-project-b") - const garden = await makeTestGarden(nonCircularProjectRoot) - expect(async () => { await garden.scanModules() }).to.not.throw() - }) - }) - - describe("detectCycles", () => { - it("should detect self-to-self cycles", () => { - const cycles = detectCycles({ - a: { a: { distance: 1, next: "a" } }, - }, ["a"]) - - expect(cycles).to.deep.eq([["a"]]) - }) - - it("should preserve dependency order when returning cycles", () => { - const cycles = detectCycles({ - foo: { bar: { distance: 1, next: "bar" } }, - bar: { baz: { distance: 1, next: "baz" } }, - baz: { foo: { distance: 1, next: "foo" } }, - }, ["foo", "bar", "baz"]) - - expect(cycles).to.deep.eq([["foo", "bar", "baz"]]) - }) - }) }) describe("addModule", () => { diff --git a/garden-service/test/src/util/validate-dependencies.ts b/garden-service/test/src/util/validate-dependencies.ts new file mode 100644 index 0000000000..6295536f3a --- /dev/null +++ b/garden-service/test/src/util/validate-dependencies.ts @@ -0,0 +1,96 @@ +import { expect } from "chai" +import { join } from "path" +import { + detectCycles, + detectMissingDependencies, + detectCircularDependencies, +} from "../../../src/util/validate-dependencies" +import { makeTestGarden, dataDir } from "../../helpers" +import { ModuleConfig } from "../../../src/config/module" +import { ConfigurationError } from "../../../src/exceptions" + +/** + * Here, we cast the garden arg to any in order to access the private moduleConfigs property. + * + * We also ignore any exeptions thrown by scanModules, because we want to more granularly + * test the validation methods below (which normally throw their exceptions during the + * execution of scanModules). + */ +async function scanAndGetConfigs(garden: any) { + try { + await garden.scanModules() + } finally { + const moduleConfigs: ModuleConfig[] = Object.values(garden.moduleConfigs) + return { + moduleConfigs, + serviceNames: Object.keys(garden.serviceNameIndex), + taskNames: Object.keys(garden.taskNameIndex), + } + } +} + +describe("validate-dependencies", () => { + describe("detectMissingDependencies", () => { + it("should return an error when a build dependency is missing", async () => { + const projectRoot = join(dataDir, "test-projects", "missing-deps", "missing-build-dep") + const garden = await makeTestGarden(projectRoot) + const { moduleConfigs, serviceNames, taskNames } = await scanAndGetConfigs(garden) + const err = detectMissingDependencies(moduleConfigs, serviceNames, taskNames) + expect(err).to.be.an.instanceOf(ConfigurationError) + }) + + it("should return an error when a runtime dependency is missing", async () => { + const projectRoot = join(dataDir, "test-projects", "missing-deps", "missing-runtime-dep") + const garden = await makeTestGarden(projectRoot) + const { moduleConfigs, serviceNames, taskNames } = await scanAndGetConfigs(garden) + const err = detectMissingDependencies(moduleConfigs, serviceNames, taskNames) + expect(err).to.be.an.instanceOf(ConfigurationError) + }) + + it("should return null when no dependencies are missing", async () => { + const projectRoot = join(dataDir, "test-project-b") + const garden = await makeTestGarden(projectRoot) + const { moduleConfigs, serviceNames, taskNames } = await scanAndGetConfigs(garden) + const err = detectMissingDependencies(moduleConfigs, serviceNames, taskNames) + expect(err).to.eql(null) + }) + }) + + describe("detectCircularDependencies", () => { + it("should return an error when circular dependencies are present", async () => { + const circularProjectRoot = join(dataDir, "test-project-circular-deps") + const garden = await makeTestGarden(circularProjectRoot) + const { moduleConfigs } = await scanAndGetConfigs(garden) + const err = detectCircularDependencies(moduleConfigs) + expect(err).to.be.an.instanceOf(ConfigurationError) + }) + + it("should return null when no circular dependencies are present", async () => { + const nonCircularProjectRoot = join(dataDir, "test-project-b") + const garden = await makeTestGarden(nonCircularProjectRoot) + const { moduleConfigs } = await scanAndGetConfigs(garden) + const err = detectCircularDependencies(moduleConfigs) + expect(err).to.eql(null) + }) + }) + + describe("detectCycles", () => { + it("should detect self-to-self cycles", () => { + const cycles = detectCycles({ + a: { a: { distance: 1, next: "a" } }, + }, ["a"]) + + expect(cycles).to.deep.eq([["a"]]) + }) + + it("should preserve dependency order when returning cycles", () => { + const cycles = detectCycles({ + foo: { bar: { distance: 1, next: "bar" } }, + bar: { baz: { distance: 1, next: "baz" } }, + baz: { foo: { distance: 1, next: "foo" } }, + }, ["foo", "bar", "baz"]) + + expect(cycles).to.deep.eq([["foo", "bar", "baz"]]) + }) + }) +})