microsoft/openvmm
Publicmirrored fromhttps://github.com/microsoft/openvmmAvailable
.github/scripts/dep-review.test.js
412lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | // Tests for dep-review.js — run with: node --test .github/scripts/dep-review.test.js |
| 5 | |
| 6 | "use strict"; |
| 7 | |
| 8 | const { describe, it } = require("node:test"); |
| 9 | const assert = require("node:assert/strict"); |
| 10 | |
| 11 | const { |
| 12 | parseExternalDeps, |
| 13 | fmtSource, |
| 14 | diffDeps, |
| 15 | buildSummary, |
| 16 | parseCratePathMap, |
| 17 | parseInternalDepGraph, |
| 18 | checkContainment, |
| 19 | diffContainmentViolations, |
| 20 | buildPolicySummary, |
| 21 | } = require("./dep-review.js"); |
| 22 | |
| 23 | // -- fixtures -- |
| 24 | |
| 25 | const LOCK_MINIMAL = ` |
| 26 | # This file is automatically @generated by Cargo. |
| 27 | version = 4 |
| 28 | |
| 29 | [[package]] |
| 30 | name = "my_crate" |
| 31 | version = "0.0.0" |
| 32 | dependencies = [ |
| 33 | "serde", |
| 34 | "helper", |
| 35 | ] |
| 36 | |
| 37 | [[package]] |
| 38 | name = "helper" |
| 39 | version = "0.0.0" |
| 40 | |
| 41 | [[package]] |
| 42 | name = "serde" |
| 43 | version = "1.0.200" |
| 44 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 45 | checksum = "abc" |
| 46 | |
| 47 | [[package]] |
| 48 | name = "serde_derive" |
| 49 | version = "1.0.200" |
| 50 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 51 | checksum = "def" |
| 52 | `; |
| 53 | |
| 54 | const MANIFEST_MINIMAL = ` |
| 55 | [workspace] |
| 56 | members = ["my_crate", "helper", "lib_a", "lib_b"] |
| 57 | |
| 58 | [workspace.dependencies] |
| 59 | my_crate = { path = "app/my_crate" } |
| 60 | helper = { path = "support/helper" } |
| 61 | lib_a = { path = "support/lib_a" } |
| 62 | lib_b = { path = "vm/lib_b" } |
| 63 | serde = "1.0" |
| 64 | `; |
| 65 | |
| 66 | const POLICY = { |
| 67 | containment: [ |
| 68 | { |
| 69 | prefix: "support/", |
| 70 | description: "support/ crates must not depend on crates outside support/", |
| 71 | }, |
| 72 | ], |
| 73 | }; |
| 74 | |
| 75 | // -- parseExternalDeps -- |
| 76 | |
| 77 | describe("parseExternalDeps", () => { |
| 78 | it("extracts external packages (those with source)", () => { |
| 79 | const deps = parseExternalDeps(LOCK_MINIMAL); |
| 80 | assert.equal(deps.size, 2); |
| 81 | assert.ok( |
| 82 | deps.has("serde\t1.0.200\tregistry+https://github.com/rust-lang/crates.io-index") |
| 83 | ); |
| 84 | assert.ok( |
| 85 | deps.has("serde_derive\t1.0.200\tregistry+https://github.com/rust-lang/crates.io-index") |
| 86 | ); |
| 87 | }); |
| 88 | |
| 89 | it("ignores internal packages (no source)", () => { |
| 90 | const deps = parseExternalDeps(LOCK_MINIMAL); |
| 91 | for (const key of deps) { |
| 92 | assert.ok(!key.startsWith("my_crate\t")); |
| 93 | assert.ok(!key.startsWith("helper\t")); |
| 94 | } |
| 95 | }); |
| 96 | |
| 97 | it("handles empty lockfile", () => { |
| 98 | const deps = parseExternalDeps("version = 4\n"); |
| 99 | assert.equal(deps.size, 0); |
| 100 | }); |
| 101 | |
| 102 | it("handles git sources", () => { |
| 103 | const lock = ` |
| 104 | [[package]] |
| 105 | name = "foo" |
| 106 | version = "0.1.0" |
| 107 | source = "git+https://github.com/org/foo?branch=main#abc123" |
| 108 | `; |
| 109 | const deps = parseExternalDeps(lock); |
| 110 | assert.equal(deps.size, 1); |
| 111 | const key = [...deps][0]; |
| 112 | assert.ok(key.startsWith("foo\t0.1.0\tgit+")); |
| 113 | }); |
| 114 | |
| 115 | it("tracks multiple versions of the same crate", () => { |
| 116 | const lock = ` |
| 117 | [[package]] |
| 118 | name = "windows-sys" |
| 119 | version = "0.48.0" |
| 120 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 121 | checksum = "aaa" |
| 122 | |
| 123 | [[package]] |
| 124 | name = "windows-sys" |
| 125 | version = "0.52.0" |
| 126 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 127 | checksum = "bbb" |
| 128 | `; |
| 129 | const deps = parseExternalDeps(lock); |
| 130 | assert.equal(deps.size, 2); |
| 131 | assert.ok( |
| 132 | deps.has("windows-sys\t0.48.0\tregistry+https://github.com/rust-lang/crates.io-index") |
| 133 | ); |
| 134 | assert.ok( |
| 135 | deps.has("windows-sys\t0.52.0\tregistry+https://github.com/rust-lang/crates.io-index") |
| 136 | ); |
| 137 | }); |
| 138 | }); |
| 139 | |
| 140 | // -- fmtSource -- |
| 141 | |
| 142 | describe("fmtSource", () => { |
| 143 | it("returns empty for registry source", () => { |
| 144 | assert.equal(fmtSource("registry+https://github.com/rust-lang/crates.io-index"), ""); |
| 145 | }); |
| 146 | |
| 147 | it("formats git source with URL", () => { |
| 148 | const result = fmtSource("git+https://github.com/org/repo?branch=main#abc123"); |
| 149 | assert.equal(result, " (https://github.com/org/repo?branch=main)"); |
| 150 | }); |
| 151 | |
| 152 | it("passes through unknown sources", () => { |
| 153 | assert.equal(fmtSource("something-else"), " (something-else)"); |
| 154 | }); |
| 155 | }); |
| 156 | |
| 157 | // -- diffDeps -- |
| 158 | |
| 159 | describe("diffDeps", () => { |
| 160 | it("detects added crates", () => { |
| 161 | const base = new Set(); |
| 162 | const pr = new Set(["foo\t1.0.0\tsrc"]); |
| 163 | const { added } = diffDeps(base, pr); |
| 164 | assert.equal(added.length, 1); |
| 165 | assert.equal(added[0].name, "foo"); |
| 166 | }); |
| 167 | |
| 168 | it("detects version changes as additions", () => { |
| 169 | const base = new Set(["foo\t1.0.0\tsrc"]); |
| 170 | const pr = new Set(["foo\t2.0.0\tsrc"]); |
| 171 | const { added } = diffDeps(base, pr); |
| 172 | assert.equal(added.length, 1); |
| 173 | assert.equal(added[0].name, "foo"); |
| 174 | assert.equal(added[0].version, "2.0.0"); |
| 175 | }); |
| 176 | |
| 177 | it("does not report removals", () => { |
| 178 | const base = new Set(["foo\t1.0.0\tsrc"]); |
| 179 | const pr = new Set(); |
| 180 | const { added } = diffDeps(base, pr); |
| 181 | assert.equal(added.length, 0); |
| 182 | }); |
| 183 | |
| 184 | it("reports nothing when identical", () => { |
| 185 | const base = new Set(["foo\t1.0.0\tsrc"]); |
| 186 | const pr = new Set(["foo\t1.0.0\tsrc"]); |
| 187 | const { added } = diffDeps(base, pr); |
| 188 | assert.equal(added.length, 0); |
| 189 | }); |
| 190 | |
| 191 | it("detects new version alongside existing version", () => { |
| 192 | const base = new Set(["foo\t1.0.0\tsrc"]); |
| 193 | const pr = new Set(["foo\t1.0.0\tsrc", "foo\t2.0.0\tsrc"]); |
| 194 | const { added } = diffDeps(base, pr); |
| 195 | assert.equal(added.length, 1); |
| 196 | assert.equal(added[0].version, "2.0.0"); |
| 197 | }); |
| 198 | }); |
| 199 | |
| 200 | // -- buildSummary -- |
| 201 | |
| 202 | describe("buildSummary", () => { |
| 203 | it("includes added crates", () => { |
| 204 | const summary = buildSummary({ |
| 205 | added: [{ name: "foo", version: "1.0.0", source: "registry+x" }], |
| 206 | }); |
| 207 | assert.ok(summary.includes("**New external crate versions:**")); |
| 208 | assert.ok(summary.includes("`foo` 1.0.0")); |
| 209 | }); |
| 210 | }); |
| 211 | |
| 212 | // -- parseCratePathMap -- |
| 213 | |
| 214 | describe("parseCratePathMap", () => { |
| 215 | it("extracts path-based dependencies", () => { |
| 216 | const map = parseCratePathMap(MANIFEST_MINIMAL); |
| 217 | assert.equal(map.get("my_crate"), "app/my_crate"); |
| 218 | assert.equal(map.get("helper"), "support/helper"); |
| 219 | assert.equal(map.get("lib_a"), "support/lib_a"); |
| 220 | assert.equal(map.get("lib_b"), "vm/lib_b"); |
| 221 | }); |
| 222 | |
| 223 | it("ignores non-path dependencies", () => { |
| 224 | const map = parseCratePathMap(MANIFEST_MINIMAL); |
| 225 | assert.ok(!map.has("serde")); |
| 226 | }); |
| 227 | |
| 228 | it("handles complex inline tables", () => { |
| 229 | const toml = `foo = { path = "a/b", features = ["x"], default-features = false }`; |
| 230 | const map = parseCratePathMap(toml); |
| 231 | assert.equal(map.get("foo"), "a/b"); |
| 232 | }); |
| 233 | }); |
| 234 | |
| 235 | // -- parseInternalDepGraph -- |
| 236 | |
| 237 | describe("parseInternalDepGraph", () => { |
| 238 | it("extracts internal crates and their dependencies", () => { |
| 239 | const { graph, internalCrates } = parseInternalDepGraph(LOCK_MINIMAL); |
| 240 | assert.ok(internalCrates.has("my_crate")); |
| 241 | assert.ok(internalCrates.has("helper")); |
| 242 | assert.ok(!internalCrates.has("serde")); |
| 243 | assert.deepEqual(graph.get("my_crate"), ["serde", "helper"]); |
| 244 | assert.deepEqual(graph.get("helper"), []); |
| 245 | }); |
| 246 | }); |
| 247 | |
| 248 | // -- checkContainment -- |
| 249 | |
| 250 | describe("checkContainment", () => { |
| 251 | it("flags violations when support/ crate depends on non-support/ crate", () => { |
| 252 | // helper (support/helper) depends on lib_b (vm/lib_b) |
| 253 | const lock = ` |
| 254 | [[package]] |
| 255 | name = "helper" |
| 256 | version = "0.0.0" |
| 257 | dependencies = [ |
| 258 | "lib_b", |
| 259 | ] |
| 260 | |
| 261 | [[package]] |
| 262 | name = "lib_b" |
| 263 | version = "0.0.0" |
| 264 | `; |
| 265 | const { graph, internalCrates } = parseInternalDepGraph(lock); |
| 266 | const pathMap = parseCratePathMap(MANIFEST_MINIMAL); |
| 267 | const violations = checkContainment(graph, internalCrates, pathMap, POLICY); |
| 268 | assert.equal(violations.length, 1); |
| 269 | assert.equal(violations[0].crate, "helper"); |
| 270 | assert.equal(violations[0].dep, "lib_b"); |
| 271 | }); |
| 272 | |
| 273 | it("allows support/ crate depending on other support/ crate", () => { |
| 274 | const lock = ` |
| 275 | [[package]] |
| 276 | name = "helper" |
| 277 | version = "0.0.0" |
| 278 | dependencies = [ |
| 279 | "lib_a", |
| 280 | ] |
| 281 | |
| 282 | [[package]] |
| 283 | name = "lib_a" |
| 284 | version = "0.0.0" |
| 285 | `; |
| 286 | const { graph, internalCrates } = parseInternalDepGraph(lock); |
| 287 | const pathMap = parseCratePathMap(MANIFEST_MINIMAL); |
| 288 | const violations = checkContainment(graph, internalCrates, pathMap, POLICY); |
| 289 | assert.equal(violations.length, 0); |
| 290 | }); |
| 291 | |
| 292 | it("ignores non-support/ crates depending on anything", () => { |
| 293 | const lock = ` |
| 294 | [[package]] |
| 295 | name = "my_crate" |
| 296 | version = "0.0.0" |
| 297 | dependencies = [ |
| 298 | "helper", |
| 299 | "lib_b", |
| 300 | ] |
| 301 | |
| 302 | [[package]] |
| 303 | name = "helper" |
| 304 | version = "0.0.0" |
| 305 | |
| 306 | [[package]] |
| 307 | name = "lib_b" |
| 308 | version = "0.0.0" |
| 309 | `; |
| 310 | const { graph, internalCrates } = parseInternalDepGraph(lock); |
| 311 | const pathMap = parseCratePathMap(MANIFEST_MINIMAL); |
| 312 | const violations = checkContainment(graph, internalCrates, pathMap, POLICY); |
| 313 | assert.equal(violations.length, 0); |
| 314 | }); |
| 315 | }); |
| 316 | |
| 317 | // -- diffContainmentViolations -- |
| 318 | |
| 319 | describe("diffContainmentViolations", () => { |
| 320 | const v1 = { crate: "a", dep: "b", cratePath: "support/a", depPath: "vm/b", rule: "test" }; |
| 321 | const v2 = { crate: "c", dep: "d", cratePath: "support/c", depPath: "vm/d", rule: "test" }; |
| 322 | |
| 323 | it("returns only new violations", () => { |
| 324 | const result = diffContainmentViolations([v1], [v1, v2]); |
| 325 | assert.equal(result.length, 1); |
| 326 | assert.equal(result[0].crate, "c"); |
| 327 | }); |
| 328 | |
| 329 | it("returns empty when no new violations", () => { |
| 330 | const result = diffContainmentViolations([v1, v2], [v1]); |
| 331 | assert.equal(result.length, 0); |
| 332 | }); |
| 333 | |
| 334 | it("returns empty when identical", () => { |
| 335 | const result = diffContainmentViolations([v1], [v1]); |
| 336 | assert.equal(result.length, 0); |
| 337 | }); |
| 338 | }); |
| 339 | |
| 340 | // -- buildPolicySummary -- |
| 341 | |
| 342 | describe("buildPolicySummary", () => { |
| 343 | it("returns empty for no violations", () => { |
| 344 | assert.equal(buildPolicySummary([]), ""); |
| 345 | }); |
| 346 | |
| 347 | it("formats violations", () => { |
| 348 | const summary = buildPolicySummary([ |
| 349 | { crate: "a", cratePath: "support/a", dep: "b", depPath: "vm/b", rule: "no cross" }, |
| 350 | ]); |
| 351 | assert.ok(summary.includes("`a` (support/a) → `b` (vm/b)")); |
| 352 | assert.ok(summary.includes("no cross")); |
| 353 | }); |
| 354 | }); |
| 355 | |
| 356 | // -- integration: full lockfile round-trip -- |
| 357 | |
| 358 | describe("integration", () => { |
| 359 | it("no issues when base and PR are identical", () => { |
| 360 | const baseDeps = parseExternalDeps(LOCK_MINIMAL); |
| 361 | const prDeps = parseExternalDeps(LOCK_MINIMAL); |
| 362 | const diff = diffDeps(baseDeps, prDeps); |
| 363 | assert.equal(diff.added.length, 0); |
| 364 | |
| 365 | const pathMap = parseCratePathMap(MANIFEST_MINIMAL); |
| 366 | const baseGraph = parseInternalDepGraph(LOCK_MINIMAL); |
| 367 | const prGraph = parseInternalDepGraph(LOCK_MINIMAL); |
| 368 | const baseV = checkContainment(baseGraph.graph, baseGraph.internalCrates, pathMap, POLICY); |
| 369 | const prV = checkContainment(prGraph.graph, prGraph.internalCrates, pathMap, POLICY); |
| 370 | const newV = diffContainmentViolations(baseV, prV); |
| 371 | assert.equal(newV.length, 0); |
| 372 | }); |
| 373 | |
| 374 | it("detects added external dep + new containment violation together", () => { |
| 375 | const prLock = LOCK_MINIMAL + ` |
| 376 | [[package]] |
| 377 | name = "new_ext" |
| 378 | version = "0.5.0" |
| 379 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 380 | checksum = "xyz" |
| 381 | |
| 382 | [[package]] |
| 383 | name = "lib_a" |
| 384 | version = "0.0.0" |
| 385 | dependencies = [ |
| 386 | "lib_b", |
| 387 | ] |
| 388 | |
| 389 | [[package]] |
| 390 | name = "lib_b" |
| 391 | version = "0.0.0" |
| 392 | `; |
| 393 | |
| 394 | // External diff |
| 395 | const baseDeps = parseExternalDeps(LOCK_MINIMAL); |
| 396 | const prDeps = parseExternalDeps(prLock); |
| 397 | const diff = diffDeps(baseDeps, prDeps); |
| 398 | assert.equal(diff.added.length, 1); |
| 399 | assert.equal(diff.added[0].name, "new_ext"); |
| 400 | |
| 401 | // Containment |
| 402 | const pathMap = parseCratePathMap(MANIFEST_MINIMAL); |
| 403 | const baseGraph = parseInternalDepGraph(LOCK_MINIMAL); |
| 404 | const prGraph = parseInternalDepGraph(prLock); |
| 405 | const baseV = checkContainment(baseGraph.graph, baseGraph.internalCrates, pathMap, POLICY); |
| 406 | const prV = checkContainment(prGraph.graph, prGraph.internalCrates, pathMap, POLICY); |
| 407 | const newV = diffContainmentViolations(baseV, prV); |
| 408 | assert.equal(newV.length, 1); |
| 409 | assert.equal(newV[0].crate, "lib_a"); |
| 410 | assert.equal(newV[0].dep, "lib_b"); |
| 411 | }); |
| 412 | }); |
| 413 | |