Skip to content

Commit 30d9aaa

Browse files
gnidantest
authored andcommitted
bin: read the world once, decide purely, write once
1 parent 8ab98cd commit 30d9aaa

2 files changed

Lines changed: 578 additions & 230 deletions

File tree

‎bin/version.test.ts‎

Lines changed: 289 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { describe, expect, it } from "vitest";
22

3-
import { parseArgs } from "./version.js";
3+
import type { Plan } from "./release/plan.js";
4+
import { versionSites } from "./release/schema-versions.js";
5+
import type { Workspace } from "./release/workspaces.js";
6+
import {
7+
check,
8+
decide,
9+
type Options,
10+
parseArgs,
11+
type Survey,
12+
} from "./version.js";
413

514
describe("parseArgs", () => {
615
it("defaults to prerelease", () => {
@@ -31,3 +40,282 @@ describe("parseArgs", () => {
3140
expect(() => parseArgs(["", "--dry-run"])).toThrow(/usage/);
3241
});
3342
});
43+
44+
const repo: Survey["repo"] = {
45+
branch: "main",
46+
dirty: false,
47+
tagsAtHead: [],
48+
nearestAnnotated: { name: "@ethdebug/format@0.1.0-draft.0", commit: "a" },
49+
nearestRelease: { name: "@ethdebug/format@0.1.0-draft.0", commit: "a" },
50+
};
51+
52+
const surveyOf = (over: Partial<Survey> = {}): Survey => ({
53+
root: "/repo",
54+
workspaces: [],
55+
tags: [],
56+
directlyChanged: [],
57+
listed: [],
58+
specTag: undefined,
59+
schemasChanged: false,
60+
schemas: [],
61+
changelogs: [],
62+
repo,
63+
...over,
64+
});
65+
66+
function workspace(name: string, version: string): Workspace {
67+
const json = { name, version };
68+
return {
69+
name,
70+
version,
71+
dir: `/repo/packages/${name.replace("@ethdebug/", "")}`,
72+
private: false,
73+
text: `${JSON.stringify(json, null, 2)}\n`,
74+
json,
75+
runtime: [],
76+
peer: [],
77+
dev: [],
78+
optional: [],
79+
dependencies: [],
80+
all: [],
81+
};
82+
}
83+
84+
const empty: Plan = { moves: [], manifests: [], schemas: [] };
85+
const options: Options = { keyword: "prerelease", all: false, dryRun: true };
86+
87+
describe("check findings", () => {
88+
it("is silent on a clean repo on main", () => {
89+
expect(check(surveyOf(), empty, options).findings).toEqual([]);
90+
});
91+
92+
it("reports a branch that is not main", () => {
93+
const survey = surveyOf({ repo: { ...repo, branch: "feature" } });
94+
expect(check(survey, empty, options).findings).toEqual([
95+
"on branch feature, not main",
96+
]);
97+
});
98+
99+
it("reports a dirty working tree", () => {
100+
const survey = surveyOf({ repo: { ...repo, dirty: true } });
101+
expect(check(survey, empty, options).findings).toEqual([
102+
"the working tree has uncommitted changes",
103+
]);
104+
});
105+
106+
it("reports a foreign annotated tag nearer than the release tag", () => {
107+
const survey = surveyOf({
108+
repo: { ...repo, nearestAnnotated: { name: "v9", commit: "b" } },
109+
});
110+
expect(check(survey, empty, options).findings).toEqual([
111+
"the nearest annotated tag v9 is not the nearest release tag " +
112+
"@ethdebug/format@0.1.0-draft.0; Lerna would miss changes " +
113+
"(a foreign tag, or a release tag that is not annotated)",
114+
]);
115+
});
116+
117+
it("reports a missing annotated release tag", () => {
118+
const survey = surveyOf({
119+
repo: { ...repo, nearestAnnotated: undefined },
120+
});
121+
expect(check(survey, empty, options).findings).toEqual([
122+
"no annotated release tag is reachable from HEAD",
123+
]);
124+
});
125+
126+
it("reports release tags at HEAD only when something changed", () => {
127+
const atHead = { ...repo, tagsAtHead: ["@ethdebug/a@0.1.0"] };
128+
expect(check(surveyOf({ repo: atHead }), empty, options).findings).toEqual(
129+
[],
130+
);
131+
132+
const loud = surveyOf({ repo: atHead, directlyChanged: ["@ethdebug/a"] });
133+
expect(check(loud, empty, options).findings).toEqual([
134+
"HEAD already carries release tags: @ethdebug/a@0.1.0; " +
135+
"Lerna skips change detection here",
136+
]);
137+
});
138+
139+
it("reports the planned tags that already exist", () => {
140+
const survey = surveyOf({ tags: ["@ethdebug/a@0.1.1", "v9"] });
141+
const plan: Plan = {
142+
...empty,
143+
moves: [
144+
{
145+
name: "@ethdebug/a",
146+
from: "0.1.0",
147+
to: "0.1.1",
148+
reason: "changed",
149+
firstRelease: false,
150+
},
151+
],
152+
};
153+
expect(check(survey, plan, options).findings).toEqual([
154+
"tags already exist: @ethdebug/a@0.1.1",
155+
]);
156+
});
157+
158+
it("emits the findings in preflight's order", () => {
159+
const survey = surveyOf({
160+
directlyChanged: ["@ethdebug/a"],
161+
tags: ["@ethdebug/a@0.1.1"],
162+
repo: {
163+
branch: "feature",
164+
dirty: true,
165+
tagsAtHead: ["@ethdebug/a@0.1.0"],
166+
nearestAnnotated: { name: "v9", commit: "b" },
167+
nearestRelease: { name: "@ethdebug/a@0.1.0", commit: "a" },
168+
},
169+
});
170+
const plan: Plan = {
171+
...empty,
172+
moves: [
173+
{
174+
name: "@ethdebug/a",
175+
from: "0.1.0",
176+
to: "0.1.1",
177+
reason: "changed",
178+
firstRelease: false,
179+
},
180+
],
181+
};
182+
const { findings } = check(survey, plan, options);
183+
expect(findings).toHaveLength(5);
184+
expect(findings[0]).toContain("on branch");
185+
expect(findings[1]).toContain("uncommitted changes");
186+
expect(findings[2]).toContain("nearest annotated tag");
187+
expect(findings[3]).toContain("HEAD already carries");
188+
expect(findings[4]).toContain("tags already exist");
189+
});
190+
});
191+
192+
const schemaText = [
193+
"examples:",
194+
" - ethdebug:",
195+
' schema: "ethdebug/format/info"',
196+
' version: "0.1.0-draft.0"',
197+
"",
198+
].join("\n");
199+
200+
const specSurvey = (version: string) =>
201+
surveyOf({
202+
workspaces: [workspace("@ethdebug/format", "0.1.0-draft.0")],
203+
tags: ["@ethdebug/format@0.1.0-draft.0"],
204+
listed: ["@ethdebug/format"],
205+
specTag: "@ethdebug/format@0.1.0-draft.0",
206+
schemasChanged: true,
207+
schemas: [
208+
{
209+
path: "schemas/info.schema.yaml",
210+
text: schemaText.replace("0.1.0-draft.0", version),
211+
sites: versionSites(schemaText.replace("0.1.0-draft.0", version)),
212+
},
213+
],
214+
});
215+
216+
describe("decide", () => {
217+
it("rewrites the manifest and schema texts to the target version", () => {
218+
const plan = decide(specSurvey("0.1.0-draft.0"), options);
219+
expect(plan.moves).toEqual([
220+
{
221+
name: "@ethdebug/format",
222+
from: "0.1.0-draft.0",
223+
to: "0.1.0-draft.1",
224+
reason: "schemas",
225+
firstRelease: false,
226+
},
227+
]);
228+
expect(plan.manifests).toEqual([
229+
{
230+
path: "packages/format/package.json",
231+
text:
232+
JSON.stringify(
233+
{ name: "@ethdebug/format", version: "0.1.0-draft.1" },
234+
null,
235+
2,
236+
) + "\n",
237+
},
238+
]);
239+
expect(plan.schemas).toEqual([
240+
{
241+
path: "schemas/info.schema.yaml",
242+
text: schemaText.replace("0.1.0-draft.0", "0.1.0-draft.1"),
243+
},
244+
]);
245+
});
246+
247+
it("counts any tag named for the workspace as released", () => {
248+
const survey = surveyOf({
249+
workspaces: [workspace("@ethdebug/a", "0.1.0")],
250+
tags: ["@ethdebug/a@not-semver"],
251+
listed: ["@ethdebug/a"],
252+
});
253+
expect(decide(survey, options).moves[0].firstRelease).toBe(false);
254+
});
255+
});
256+
257+
describe("check errors", () => {
258+
it("reports a drifted schema site when the spec package moves", () => {
259+
const survey = specSurvey("0.0.9");
260+
const plan = decide(survey, options);
261+
expect(check(survey, plan, options).errors).toEqual([
262+
"schemas/info.schema.yaml:4: examples/0/ethdebug names 0.0.9, " +
263+
"expected 0.1.0-draft.0",
264+
]);
265+
});
266+
267+
it("reports zero sites when the spec package moves", () => {
268+
const survey = surveyOf({
269+
...specSurvey("0.1.0-draft.0"),
270+
schemas: [{ path: "schemas/x.schema.yaml", text: "", sites: [] }],
271+
});
272+
const plan = decide(survey, options);
273+
expect(check(survey, plan, options).errors).toEqual([
274+
"schemas/: no example names the specification version; " +
275+
"the release would rewrite nothing",
276+
]);
277+
});
278+
279+
it("puts the keyword guards before the plan problems", () => {
280+
const survey = surveyOf({
281+
workspaces: [workspace("@ethdebug/a", "0.1.0-preview.1")],
282+
});
283+
const backwards: Plan = {
284+
...empty,
285+
moves: [
286+
{
287+
name: "@ethdebug/a",
288+
from: "0.1.0-preview.1",
289+
to: "0.1.0-preview.0",
290+
reason: "changed",
291+
firstRelease: false,
292+
},
293+
],
294+
};
295+
const minor: Options = { keyword: "minor", all: false, dryRun: true };
296+
const errors = check(survey, backwards, minor).errors;
297+
expect(errors).toEqual([
298+
"minor starts a series for every workspace: pass --all",
299+
"cannot start a series while @ethdebug/a is a prerelease; " +
300+
"run `patch` first",
301+
"@ethdebug/a: 0.1.0-preview.0 does not sort after 0.1.0-preview.1",
302+
]);
303+
});
304+
});
305+
306+
describe("check changelogs", () => {
307+
it("reads the planned changelogs from the survey", () => {
308+
const survey = surveyOf({
309+
...specSurvey("0.1.0-draft.0"),
310+
changelogs: [
311+
{ path: "CHANGELOG.md", text: "## 0.1.0-draft.1\n\n- entry\n" },
312+
{ path: "packages/format/CHANGELOG.md", text: undefined },
313+
{ path: "packages/other/CHANGELOG.md", text: undefined },
314+
],
315+
});
316+
const plan = decide(survey, options);
317+
expect(check(survey, plan, options).changelogs).toEqual([
318+
"packages/format/CHANGELOG.md: file is missing",
319+
]);
320+
});
321+
});

0 commit comments

Comments
 (0)