Skip to content

Commit dfa1339

Browse files
Improve version parsing - parse Xcode version from version plist (#10)
* rework xcode version retrieving * debug logs * fix version parsing * rebuild task * more debug * more debug * Update test.yml * improve logging * Update test.yml
1 parent cfeae3d commit dfa1339

10 files changed

+315
-200
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: macos-latest
1414
strategy:
1515
matrix:
16-
xcode-version: ['10.3', '11', '11.2', '11.4.0', '11.4.1', '^11.4.0', latest, latest-stable]
16+
xcode-version: ['10.3', '11', '11.2', '11.4.1', '11.7', '12', '12.0', '12.2', '^11.4.0', '~11.4.0', latest, latest-stable]
1717
fail-fast: false
1818
steps:
1919
- name: Checkout
File renamed without changes.
File renamed without changes.

__tests__/data/xcode-version.plist

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>BuildAliasOf</key>
6+
<string>IDEFrameworks</string>
7+
<key>BuildVersion</key>
8+
<string>4</string>
9+
<key>CFBundleShortVersionString</key>
10+
<string>12.0.1</string>
11+
<key>CFBundleVersion</key>
12+
<string>17220</string>
13+
<key>ProductBuildVersion</key>
14+
<string>12A7300</string>
15+
<key>ProjectName</key>
16+
<string>IDEFrameworks</string>
17+
<key>SourceVersion</key>
18+
<string>17220000000000000</string>
19+
</dict>
20+
</plist>

__tests__/xcode-selector.test.ts

+33-71
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,42 @@
1-
import * as fs from "fs";
2-
import * as child from "child_process";
1+
import fs from "fs";
2+
import child from "child_process";
33
import * as core from "@actions/core";
4-
import { XcodeSelector, XcodeVersion } from "../src/xcode-selector";
4+
import { XcodeSelector } from "../src/xcode-selector";
55
import * as xcodeUtils from "../src/xcode-utils";
66

7-
jest.mock("fs");
87
jest.mock("child_process");
9-
jest.mock("@actions/core");
10-
jest.mock("../src/xcode-utils");
118

12-
const buildFsDirentItem = (name: string, opt: { isSymbolicLink: boolean; isDirectory: boolean }): fs.Dirent => {
13-
return {
14-
name,
15-
isSymbolicLink: () => opt.isSymbolicLink,
16-
isDirectory: () => opt.isDirectory
17-
} as fs.Dirent;
18-
};
19-
20-
const fakeReadDirResults = [
21-
buildFsDirentItem("Xcode.app", { isSymbolicLink: true, isDirectory: false }),
22-
buildFsDirentItem("Xcode.app", { isSymbolicLink: false, isDirectory: true }),
23-
buildFsDirentItem("Xcode_11.1.app", { isSymbolicLink: false, isDirectory: true }),
24-
buildFsDirentItem("Xcode_11.1_beta.app", { isSymbolicLink: true, isDirectory: false }),
25-
buildFsDirentItem("Xcode_11.2.1.app", { isSymbolicLink: false, isDirectory: true }),
26-
buildFsDirentItem("Xcode_11.4.app", { isSymbolicLink: true, isDirectory: false }),
27-
buildFsDirentItem("Xcode_11.4_beta.app", { isSymbolicLink: false, isDirectory: true }),
28-
buildFsDirentItem("Xcode_11.app", { isSymbolicLink: false, isDirectory: true }),
29-
buildFsDirentItem("Xcode_12_beta.app", { isSymbolicLink: false, isDirectory: true }),
30-
buildFsDirentItem("third_party_folder", { isSymbolicLink: false, isDirectory: true }),
9+
const fakeGetXcodeVersionInfoResult: xcodeUtils.XcodeVersion[] = [
10+
{ version: "10.3.0", buildNumber: "", path: "/Applications/Xcode_10.3.app", releaseType: "GM", stable: true },
11+
{ version: "12.0.0", buildNumber: "", path: "/Applications/Xcode_12_beta.app", releaseType: "Beta", stable: false },
12+
{ version: "11.2.1", buildNumber: "", path: "/Applications/Xcode_11.2.1.app", releaseType: "GM", stable: true },
13+
{ version: "11.4.0", buildNumber: "", path: "/Applications/Xcode_11.4.app", releaseType: "GM", stable: true },
14+
{ version: "11.0.0", buildNumber: "", path: "/Applications/Xcode_11.app", releaseType: "GM", stable: true },
15+
{ version: "11.2.0", buildNumber: "", path: "/Applications/Xcode_11.2.app", releaseType: "GM", stable: true },
3116
];
32-
33-
const fakeGetVersionsResult: XcodeVersion[] = [
34-
{ version: "12.0.0", path: "", stable: false },
35-
{ version: "11.4.0", path: "", stable: true },
36-
{ version: "11.2.1", path: "", stable: true },
37-
{ version: "11.2.0", path: "", stable: true },
38-
{ version: "11.0.0", path: "", stable: true },
39-
{ version: "10.3.0", path: "", stable: true }
17+
const fakeGetInstalledXcodeAppsResult: string[] = [
18+
"/Applications/Xcode_10.3.app",
19+
"/Applications/Xcode_12_beta.app",
20+
"/Applications/Xcode_11.2.1.app",
21+
"/Applications/Xcode_11.4.app",
22+
"/Applications/Xcode_11.app",
23+
"/Applications/Xcode_11.2.app",
24+
"/Applications/Xcode_fake_path.app"
25+
];
26+
const expectedGetAllVersionsResult: xcodeUtils.XcodeVersion[] = [
27+
{ version: "12.0.0", buildNumber: "", path: "/Applications/Xcode_12_beta.app", releaseType: "Beta", stable: false },
28+
{ version: "11.4.0", buildNumber: "", path: "/Applications/Xcode_11.4.app", releaseType: "GM", stable: true },
29+
{ version: "11.2.1", buildNumber: "", path: "/Applications/Xcode_11.2.1.app", releaseType: "GM", stable: true },
30+
{ version: "11.2.0", buildNumber: "", path: "/Applications/Xcode_11.2.app", releaseType: "GM", stable: true },
31+
{ version: "11.0.0", buildNumber: "", path: "/Applications/Xcode_11.app", releaseType: "GM", stable: true },
32+
{ version: "10.3.0", buildNumber: "", path: "/Applications/Xcode_10.3.app", releaseType: "GM", stable: true },
4033
];
4134

4235
describe("XcodeSelector", () => {
43-
describe("getXcodeVersionFromAppPath", () => {
44-
beforeEach(() => {
45-
jest.spyOn(xcodeUtils, "getXcodeReleaseType").mockImplementation(() => xcodeUtils.XcodeReleaseType.GM);
46-
});
47-
48-
afterEach(() => {
49-
jest.resetAllMocks();
50-
jest.clearAllMocks();
51-
});
52-
53-
it.each([
54-
["/temp/Xcode_11.app", { version: "11.0.0", path: "/temp/Xcode_11.app", stable: true }],
55-
["/temp/Xcode_11.2.app", { version: "11.2.0", path: "/temp/Xcode_11.2.app", stable: true }],
56-
["/temp/Xcode_11.2.1.app", { version: "11.2.1", path: "/temp/Xcode_11.2.1.app", stable: true }],
57-
["/temp/Xcode_11.2.1_beta.app", { version: "11.2.1", path: "/temp/Xcode_11.2.1_beta.app", stable: true }],
58-
["/temp/Xcode.app", null],
59-
["/temp/Xcode_11.2", null],
60-
["/temp/Xcode.11.2.app", null]
61-
])("'%s' -> '%s'", (input: string, expected: XcodeVersion | null) => {
62-
// test private method
63-
const actual = new XcodeSelector()["getXcodeVersionFromAppPath"](input);
64-
expect(actual).toEqual(expected);
65-
});
66-
67-
});
68-
6936
describe("getAllVersions", () => {
7037
beforeEach(() => {
71-
jest.spyOn(fs, "readdirSync").mockImplementation(() => fakeReadDirResults);
72-
jest.spyOn(xcodeUtils, "getXcodeReleaseType").mockImplementation(() => xcodeUtils.XcodeReleaseType.GM);
38+
jest.spyOn(xcodeUtils, "getInstalledXcodeApps").mockImplementation(() => fakeGetInstalledXcodeAppsResult);
39+
jest.spyOn(xcodeUtils, "getXcodeVersionInfo").mockImplementation((path) => fakeGetXcodeVersionInfoResult.find(app => app.path === path) ?? null);
7340
});
7441

7542
afterEach(() => {
@@ -79,14 +46,7 @@ describe("XcodeSelector", () => {
7946

8047
it("versions are filtered correctly", () => {
8148
const sel = new XcodeSelector();
82-
const expectedVersions: XcodeVersion[] = [
83-
{ version: "12.0.0", path: "/Applications/Xcode_12_beta.app", stable: true},
84-
{ version: "11.4.0", path: "/Applications/Xcode_11.4_beta.app", stable: true },
85-
{ version: "11.2.1", path: "/Applications/Xcode_11.2.1.app", stable: true },
86-
{ version: "11.1.0", path: "/Applications/Xcode_11.1.app", stable: true },
87-
{ version: "11.0.0", path: "/Applications/Xcode_11.app", stable: true },
88-
];
89-
expect(sel.getAllVersions()).toEqual(expectedVersions);
49+
expect(sel.getAllVersions()).toEqual(expectedGetAllVersionsResult);
9050
});
9151
});
9252

@@ -106,7 +66,7 @@ describe("XcodeSelector", () => {
10666
["give me latest version", null]
10767
] as [string, string | null][])("'%s' -> '%s'", (versionSpec: string, expected: string | null) => {
10868
const sel = new XcodeSelector();
109-
sel.getAllVersions = (): XcodeVersion[] => fakeGetVersionsResult;
69+
sel.getAllVersions = (): xcodeUtils.XcodeVersion[] => expectedGetAllVersionsResult;
11070
const matchedVersion = sel.findVersion(versionSpec)?.version ?? null;
11171
expect(matchedVersion).toBe(expected);
11272
});
@@ -116,8 +76,10 @@ describe("XcodeSelector", () => {
11676
let coreExportVariableSpy: jest.SpyInstance;
11777
let fsExistsSpy: jest.SpyInstance;
11878
let fsSpawnSpy: jest.SpyInstance;
119-
const xcodeVersion: XcodeVersion = {
79+
const xcodeVersion: xcodeUtils.XcodeVersion = {
12080
version: "11.4",
81+
buildNumber: "12A7300",
82+
releaseType: "GM",
12183
path: "/Applications/Xcode_11.4.app",
12284
stable: true
12385
};

__tests__/xcode-utils.test.ts

+153-29
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,173 @@
1-
import * as path from "path";
2-
import { getXcodeReleaseType, XcodeReleaseType } from "../src/xcode-utils";
1+
import fs from "fs";
2+
import path from "path";
3+
import * as xcodeUtils from "../src/xcode-utils";
34

4-
jest.mock("path");
5+
let pathJoinSpy: jest.SpyInstance;
6+
let readdirSyncSpy: jest.SpyInstance;
7+
let getXcodeReleaseTypeSpy: jest.SpyInstance;
8+
let parsePlistFileSpy: jest.SpyInstance;
59

6-
describe("getXcodeReleaseType", () => {
7-
const buildPlistPath = (plistName: string) => {
8-
return `${__dirname}/data/${plistName}`;
9-
};
10+
const buildPlistPath = (plistName: string) => {
11+
return `${__dirname}/data/${plistName}`;
12+
};
13+
14+
const buildFsDirentItem = (name: string, opt: { isSymbolicLink: boolean; isDirectory: boolean }): fs.Dirent => {
15+
return {
16+
name,
17+
isSymbolicLink: () => opt.isSymbolicLink,
18+
isDirectory: () => opt.isDirectory
19+
} as fs.Dirent;
20+
};
1021

11-
let pathJoinSpy: jest.SpyInstance;
22+
const fakeReadDirResults = [
23+
buildFsDirentItem("Xcode_2.app", { isSymbolicLink: true, isDirectory: false }),
24+
buildFsDirentItem("Xcode.app", { isSymbolicLink: false, isDirectory: true }),
25+
buildFsDirentItem("Xcode_11.1.app", { isSymbolicLink: false, isDirectory: true }),
26+
buildFsDirentItem("Xcode_11.1_beta.app", { isSymbolicLink: true, isDirectory: false }),
27+
buildFsDirentItem("Xcode_11.2.1.app", { isSymbolicLink: false, isDirectory: true }),
28+
buildFsDirentItem("Xcode_11.4.app", { isSymbolicLink: true, isDirectory: false }),
29+
buildFsDirentItem("Xcode_11.4_beta.app", { isSymbolicLink: false, isDirectory: true }),
30+
buildFsDirentItem("Xcode_11.app", { isSymbolicLink: false, isDirectory: true }),
31+
buildFsDirentItem("Xcode_12_beta.app", { isSymbolicLink: false, isDirectory: true }),
32+
buildFsDirentItem("third_party_folder", { isSymbolicLink: false, isDirectory: true }),
33+
];
1234

35+
describe("getInstalledXcodeApps", () => {
1336
beforeEach(() => {
14-
pathJoinSpy = jest.spyOn(path, "join");
37+
readdirSyncSpy = jest.spyOn(fs, "readdirSync");
1538
});
1639

17-
it("stable release", () => {
18-
const plistPath = buildPlistPath("xcode-stable.plist");
19-
pathJoinSpy.mockImplementation(() => plistPath);
20-
const releaseType = getXcodeReleaseType("");
21-
expect(releaseType).toBe(XcodeReleaseType.GM);
40+
it("versions are filtered correctly", () => {
41+
readdirSyncSpy.mockImplementation(() => fakeReadDirResults);
42+
const expectedVersions: string[] = [
43+
"/Applications/Xcode_11.1.app",
44+
"/Applications/Xcode_11.2.1.app",
45+
"/Applications/Xcode_11.4_beta.app",
46+
"/Applications/Xcode_11.app",
47+
"/Applications/Xcode_12_beta.app",
48+
];
49+
50+
const installedXcodeApps = xcodeUtils.getInstalledXcodeApps();
51+
expect(installedXcodeApps).toEqual(expectedVersions);
2252
});
2353

24-
it("beta release", () => {
25-
const plistPath = buildPlistPath("xcode-beta.plist");
26-
pathJoinSpy.mockImplementation(() => plistPath);
27-
const releaseType = getXcodeReleaseType("");
28-
expect(releaseType).toBe(XcodeReleaseType.Beta);
54+
afterEach(() => {
55+
jest.resetAllMocks();
56+
jest.clearAllMocks();
2957
});
58+
});
3059

31-
it("unknown release", () => {
32-
const plistPath = buildPlistPath("xcode-empty-license.plist");
33-
pathJoinSpy.mockImplementation(() => plistPath);
34-
const releaseType = getXcodeReleaseType("");
35-
expect(releaseType).toBe(XcodeReleaseType.Unknown);
60+
describe("getXcodeReleaseType", () => {
61+
beforeEach(() => {
62+
pathJoinSpy = jest.spyOn(path, "join");
3663
});
3764

38-
it("non-existent plist", () => {
39-
const plistPath = buildPlistPath("xcode-fake.plist");
65+
it.each([
66+
["xcode-stable-license.plist", "GM"],
67+
["xcode-beta-license.plist", "Beta"],
68+
["xcode-empty-license.plist", "Unknown"],
69+
["xcode-fake.plist", "Unknown"],
70+
] as [string, xcodeUtils.XcodeVersionReleaseType][])("%s -> %s", (plistName: string, expected: xcodeUtils.XcodeVersionReleaseType) => {
71+
const plistPath = buildPlistPath(plistName);
4072
pathJoinSpy.mockImplementation(() => plistPath);
41-
const releaseType = getXcodeReleaseType("");
42-
expect(releaseType).toBe(XcodeReleaseType.Unknown);
73+
const releaseType = xcodeUtils.getXcodeReleaseType("");
74+
expect(releaseType).toBe(expected);
75+
});
76+
77+
afterEach(() => {
78+
jest.resetAllMocks();
79+
jest.clearAllMocks();
80+
});
81+
});
82+
83+
describe("getXcodeVersionInfo", () => {
84+
beforeEach(() => {
85+
pathJoinSpy = jest.spyOn(path, "join");
86+
getXcodeReleaseTypeSpy = jest.spyOn(xcodeUtils, "getXcodeReleaseType");
4387
});
4488

4589
afterEach(() => {
4690
jest.resetAllMocks();
4791
jest.clearAllMocks();
4892
});
49-
});
93+
94+
it("read version from plist", () => {
95+
const plistPath = buildPlistPath("xcode-version.plist");
96+
pathJoinSpy.mockImplementation(() => plistPath);
97+
getXcodeReleaseTypeSpy.mockImplementation(() => "GM");
98+
99+
const expected: xcodeUtils.XcodeVersion = {
100+
version: "12.0.1",
101+
buildNumber: "12A7300",
102+
path: "fake_path",
103+
releaseType: "GM",
104+
stable: true
105+
};
106+
107+
const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path");
108+
expect(xcodeInfo).toEqual(expected);
109+
});
110+
111+
describe("'stable' property", () => {
112+
it.each([
113+
["GM", true],
114+
["Beta", false],
115+
["Unknown", false]
116+
])("%s -> %s", (releaseType: string, expected: boolean) => {
117+
const plistPath = buildPlistPath("xcode-version.plist");
118+
pathJoinSpy.mockImplementation(() => plistPath);
119+
getXcodeReleaseTypeSpy.mockImplementation(() => releaseType);
120+
121+
const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path");
122+
expect(xcodeInfo).toBeTruthy();
123+
expect(xcodeInfo?.stable).toBe(expected);
124+
});
125+
});
126+
127+
describe("coerce validation", () => {
128+
beforeEach(() => {
129+
parsePlistFileSpy = jest.spyOn(xcodeUtils, "parsePlistFile");
130+
});
131+
132+
afterEach(() => {
133+
jest.resetAllMocks();
134+
jest.clearAllMocks();
135+
});
136+
137+
it("full version", () => {
138+
parsePlistFileSpy.mockImplementation(() => {
139+
return {
140+
CFBundleShortVersionString: "12.0.1", ProductBuildVersion: "2FF"
141+
};
142+
});
143+
getXcodeReleaseTypeSpy.mockImplementation(() => "GM");
144+
145+
const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path");
146+
expect(xcodeInfo?.version).toBe("12.0.1");
147+
});
148+
149+
it("partial version", () => {
150+
parsePlistFileSpy.mockImplementation(() => {
151+
return {
152+
CFBundleShortVersionString: "10.3", ProductBuildVersion: "2FF"
153+
};
154+
});
155+
getXcodeReleaseTypeSpy.mockImplementation(() => "GM");
156+
157+
const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path");
158+
expect(xcodeInfo?.version).toBe("10.3.0");
159+
});
160+
161+
it("invalid version", () => {
162+
parsePlistFileSpy.mockImplementation(() => {
163+
return {
164+
CFBundleShortVersionString: "fake_version", ProductBuildVersion: "2FF"
165+
};
166+
});
167+
getXcodeReleaseTypeSpy.mockImplementation(() => "GM");
168+
169+
const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path");
170+
expect(xcodeInfo).toBeNull();
171+
});
172+
});
173+
});

0 commit comments

Comments
 (0)