Skip to content

Commit

Permalink
Improve version parsing - parse Xcode version from version plist (#10)
Browse files Browse the repository at this point in the history
* rework xcode version retrieving

* debug logs

* fix version parsing

* rebuild task

* more debug

* more debug

* Update test.yml

* improve logging

* Update test.yml
  • Loading branch information
maxim-lobanov committed Oct 15, 2020
1 parent cfeae3d commit dfa1339
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 200 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
xcode-version: ['10.3', '11', '11.2', '11.4.0', '11.4.1', '^11.4.0', latest, latest-stable]
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]
fail-fast: false
steps:
- name: Checkout
Expand Down
File renamed without changes.
File renamed without changes.
20 changes: 20 additions & 0 deletions __tests__/data/xcode-version.plist
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildAliasOf</key>
<string>IDEFrameworks</string>
<key>BuildVersion</key>
<string>4</string>
<key>CFBundleShortVersionString</key>
<string>12.0.1</string>
<key>CFBundleVersion</key>
<string>17220</string>
<key>ProductBuildVersion</key>
<string>12A7300</string>
<key>ProjectName</key>
<string>IDEFrameworks</string>
<key>SourceVersion</key>
<string>17220000000000000</string>
</dict>
</plist>
104 changes: 33 additions & 71 deletions __tests__/xcode-selector.test.ts
@@ -1,75 +1,42 @@
import * as fs from "fs";
import * as child from "child_process";
import fs from "fs";
import child from "child_process";
import * as core from "@actions/core";
import { XcodeSelector, XcodeVersion } from "../src/xcode-selector";
import { XcodeSelector } from "../src/xcode-selector";
import * as xcodeUtils from "../src/xcode-utils";

jest.mock("fs");
jest.mock("child_process");
jest.mock("@actions/core");
jest.mock("../src/xcode-utils");

const buildFsDirentItem = (name: string, opt: { isSymbolicLink: boolean; isDirectory: boolean }): fs.Dirent => {
return {
name,
isSymbolicLink: () => opt.isSymbolicLink,
isDirectory: () => opt.isDirectory
} as fs.Dirent;
};

const fakeReadDirResults = [
buildFsDirentItem("Xcode.app", { isSymbolicLink: true, isDirectory: false }),
buildFsDirentItem("Xcode.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("Xcode_11.1.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("Xcode_11.1_beta.app", { isSymbolicLink: true, isDirectory: false }),
buildFsDirentItem("Xcode_11.2.1.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("Xcode_11.4.app", { isSymbolicLink: true, isDirectory: false }),
buildFsDirentItem("Xcode_11.4_beta.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("Xcode_11.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("Xcode_12_beta.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("third_party_folder", { isSymbolicLink: false, isDirectory: true }),
const fakeGetXcodeVersionInfoResult: xcodeUtils.XcodeVersion[] = [
{ version: "10.3.0", buildNumber: "", path: "/Applications/Xcode_10.3.app", releaseType: "GM", stable: true },
{ version: "12.0.0", buildNumber: "", path: "/Applications/Xcode_12_beta.app", releaseType: "Beta", stable: false },
{ version: "11.2.1", buildNumber: "", path: "/Applications/Xcode_11.2.1.app", releaseType: "GM", stable: true },
{ version: "11.4.0", buildNumber: "", path: "/Applications/Xcode_11.4.app", releaseType: "GM", stable: true },
{ version: "11.0.0", buildNumber: "", path: "/Applications/Xcode_11.app", releaseType: "GM", stable: true },
{ version: "11.2.0", buildNumber: "", path: "/Applications/Xcode_11.2.app", releaseType: "GM", stable: true },
];

const fakeGetVersionsResult: XcodeVersion[] = [
{ version: "12.0.0", path: "", stable: false },
{ version: "11.4.0", path: "", stable: true },
{ version: "11.2.1", path: "", stable: true },
{ version: "11.2.0", path: "", stable: true },
{ version: "11.0.0", path: "", stable: true },
{ version: "10.3.0", path: "", stable: true }
const fakeGetInstalledXcodeAppsResult: string[] = [
"/Applications/Xcode_10.3.app",
"/Applications/Xcode_12_beta.app",
"/Applications/Xcode_11.2.1.app",
"/Applications/Xcode_11.4.app",
"/Applications/Xcode_11.app",
"/Applications/Xcode_11.2.app",
"/Applications/Xcode_fake_path.app"
];
const expectedGetAllVersionsResult: xcodeUtils.XcodeVersion[] = [
{ version: "12.0.0", buildNumber: "", path: "/Applications/Xcode_12_beta.app", releaseType: "Beta", stable: false },
{ version: "11.4.0", buildNumber: "", path: "/Applications/Xcode_11.4.app", releaseType: "GM", stable: true },
{ version: "11.2.1", buildNumber: "", path: "/Applications/Xcode_11.2.1.app", releaseType: "GM", stable: true },
{ version: "11.2.0", buildNumber: "", path: "/Applications/Xcode_11.2.app", releaseType: "GM", stable: true },
{ version: "11.0.0", buildNumber: "", path: "/Applications/Xcode_11.app", releaseType: "GM", stable: true },
{ version: "10.3.0", buildNumber: "", path: "/Applications/Xcode_10.3.app", releaseType: "GM", stable: true },
];

describe("XcodeSelector", () => {
describe("getXcodeVersionFromAppPath", () => {
beforeEach(() => {
jest.spyOn(xcodeUtils, "getXcodeReleaseType").mockImplementation(() => xcodeUtils.XcodeReleaseType.GM);
});

afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});

it.each([
["/temp/Xcode_11.app", { version: "11.0.0", path: "/temp/Xcode_11.app", stable: true }],
["/temp/Xcode_11.2.app", { version: "11.2.0", path: "/temp/Xcode_11.2.app", stable: true }],
["/temp/Xcode_11.2.1.app", { version: "11.2.1", path: "/temp/Xcode_11.2.1.app", stable: true }],
["/temp/Xcode_11.2.1_beta.app", { version: "11.2.1", path: "/temp/Xcode_11.2.1_beta.app", stable: true }],
["/temp/Xcode.app", null],
["/temp/Xcode_11.2", null],
["/temp/Xcode.11.2.app", null]
])("'%s' -> '%s'", (input: string, expected: XcodeVersion | null) => {
// test private method
const actual = new XcodeSelector()["getXcodeVersionFromAppPath"](input);
expect(actual).toEqual(expected);
});

});

describe("getAllVersions", () => {
beforeEach(() => {
jest.spyOn(fs, "readdirSync").mockImplementation(() => fakeReadDirResults);
jest.spyOn(xcodeUtils, "getXcodeReleaseType").mockImplementation(() => xcodeUtils.XcodeReleaseType.GM);
jest.spyOn(xcodeUtils, "getInstalledXcodeApps").mockImplementation(() => fakeGetInstalledXcodeAppsResult);
jest.spyOn(xcodeUtils, "getXcodeVersionInfo").mockImplementation((path) => fakeGetXcodeVersionInfoResult.find(app => app.path === path) ?? null);
});

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

it("versions are filtered correctly", () => {
const sel = new XcodeSelector();
const expectedVersions: XcodeVersion[] = [
{ version: "12.0.0", path: "/Applications/Xcode_12_beta.app", stable: true},
{ version: "11.4.0", path: "/Applications/Xcode_11.4_beta.app", stable: true },
{ version: "11.2.1", path: "/Applications/Xcode_11.2.1.app", stable: true },
{ version: "11.1.0", path: "/Applications/Xcode_11.1.app", stable: true },
{ version: "11.0.0", path: "/Applications/Xcode_11.app", stable: true },
];
expect(sel.getAllVersions()).toEqual(expectedVersions);
expect(sel.getAllVersions()).toEqual(expectedGetAllVersionsResult);
});
});

Expand All @@ -106,7 +66,7 @@ describe("XcodeSelector", () => {
["give me latest version", null]
] as [string, string | null][])("'%s' -> '%s'", (versionSpec: string, expected: string | null) => {
const sel = new XcodeSelector();
sel.getAllVersions = (): XcodeVersion[] => fakeGetVersionsResult;
sel.getAllVersions = (): xcodeUtils.XcodeVersion[] => expectedGetAllVersionsResult;
const matchedVersion = sel.findVersion(versionSpec)?.version ?? null;
expect(matchedVersion).toBe(expected);
});
Expand All @@ -116,8 +76,10 @@ describe("XcodeSelector", () => {
let coreExportVariableSpy: jest.SpyInstance;
let fsExistsSpy: jest.SpyInstance;
let fsSpawnSpy: jest.SpyInstance;
const xcodeVersion: XcodeVersion = {
const xcodeVersion: xcodeUtils.XcodeVersion = {
version: "11.4",
buildNumber: "12A7300",
releaseType: "GM",
path: "/Applications/Xcode_11.4.app",
stable: true
};
Expand Down
182 changes: 153 additions & 29 deletions __tests__/xcode-utils.test.ts
@@ -1,49 +1,173 @@
import * as path from "path";
import { getXcodeReleaseType, XcodeReleaseType } from "../src/xcode-utils";
import fs from "fs";
import path from "path";
import * as xcodeUtils from "../src/xcode-utils";

jest.mock("path");
let pathJoinSpy: jest.SpyInstance;
let readdirSyncSpy: jest.SpyInstance;
let getXcodeReleaseTypeSpy: jest.SpyInstance;
let parsePlistFileSpy: jest.SpyInstance;

describe("getXcodeReleaseType", () => {
const buildPlistPath = (plistName: string) => {
return `${__dirname}/data/${plistName}`;
};
const buildPlistPath = (plistName: string) => {
return `${__dirname}/data/${plistName}`;
};

const buildFsDirentItem = (name: string, opt: { isSymbolicLink: boolean; isDirectory: boolean }): fs.Dirent => {
return {
name,
isSymbolicLink: () => opt.isSymbolicLink,
isDirectory: () => opt.isDirectory
} as fs.Dirent;
};

let pathJoinSpy: jest.SpyInstance;
const fakeReadDirResults = [
buildFsDirentItem("Xcode_2.app", { isSymbolicLink: true, isDirectory: false }),
buildFsDirentItem("Xcode.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("Xcode_11.1.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("Xcode_11.1_beta.app", { isSymbolicLink: true, isDirectory: false }),
buildFsDirentItem("Xcode_11.2.1.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("Xcode_11.4.app", { isSymbolicLink: true, isDirectory: false }),
buildFsDirentItem("Xcode_11.4_beta.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("Xcode_11.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("Xcode_12_beta.app", { isSymbolicLink: false, isDirectory: true }),
buildFsDirentItem("third_party_folder", { isSymbolicLink: false, isDirectory: true }),
];

describe("getInstalledXcodeApps", () => {
beforeEach(() => {
pathJoinSpy = jest.spyOn(path, "join");
readdirSyncSpy = jest.spyOn(fs, "readdirSync");
});

it("stable release", () => {
const plistPath = buildPlistPath("xcode-stable.plist");
pathJoinSpy.mockImplementation(() => plistPath);
const releaseType = getXcodeReleaseType("");
expect(releaseType).toBe(XcodeReleaseType.GM);
it("versions are filtered correctly", () => {
readdirSyncSpy.mockImplementation(() => fakeReadDirResults);
const expectedVersions: string[] = [
"/Applications/Xcode_11.1.app",
"/Applications/Xcode_11.2.1.app",
"/Applications/Xcode_11.4_beta.app",
"/Applications/Xcode_11.app",
"/Applications/Xcode_12_beta.app",
];

const installedXcodeApps = xcodeUtils.getInstalledXcodeApps();
expect(installedXcodeApps).toEqual(expectedVersions);
});

it("beta release", () => {
const plistPath = buildPlistPath("xcode-beta.plist");
pathJoinSpy.mockImplementation(() => plistPath);
const releaseType = getXcodeReleaseType("");
expect(releaseType).toBe(XcodeReleaseType.Beta);
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
});

it("unknown release", () => {
const plistPath = buildPlistPath("xcode-empty-license.plist");
pathJoinSpy.mockImplementation(() => plistPath);
const releaseType = getXcodeReleaseType("");
expect(releaseType).toBe(XcodeReleaseType.Unknown);
describe("getXcodeReleaseType", () => {
beforeEach(() => {
pathJoinSpy = jest.spyOn(path, "join");
});

it("non-existent plist", () => {
const plistPath = buildPlistPath("xcode-fake.plist");
it.each([
["xcode-stable-license.plist", "GM"],
["xcode-beta-license.plist", "Beta"],
["xcode-empty-license.plist", "Unknown"],
["xcode-fake.plist", "Unknown"],
] as [string, xcodeUtils.XcodeVersionReleaseType][])("%s -> %s", (plistName: string, expected: xcodeUtils.XcodeVersionReleaseType) => {
const plistPath = buildPlistPath(plistName);
pathJoinSpy.mockImplementation(() => plistPath);
const releaseType = getXcodeReleaseType("");
expect(releaseType).toBe(XcodeReleaseType.Unknown);
const releaseType = xcodeUtils.getXcodeReleaseType("");
expect(releaseType).toBe(expected);
});

afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
});

describe("getXcodeVersionInfo", () => {
beforeEach(() => {
pathJoinSpy = jest.spyOn(path, "join");
getXcodeReleaseTypeSpy = jest.spyOn(xcodeUtils, "getXcodeReleaseType");
});

afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
});

it("read version from plist", () => {
const plistPath = buildPlistPath("xcode-version.plist");
pathJoinSpy.mockImplementation(() => plistPath);
getXcodeReleaseTypeSpy.mockImplementation(() => "GM");

const expected: xcodeUtils.XcodeVersion = {
version: "12.0.1",
buildNumber: "12A7300",
path: "fake_path",
releaseType: "GM",
stable: true
};

const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path");
expect(xcodeInfo).toEqual(expected);
});

describe("'stable' property", () => {
it.each([
["GM", true],
["Beta", false],
["Unknown", false]
])("%s -> %s", (releaseType: string, expected: boolean) => {
const plistPath = buildPlistPath("xcode-version.plist");
pathJoinSpy.mockImplementation(() => plistPath);
getXcodeReleaseTypeSpy.mockImplementation(() => releaseType);

const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path");
expect(xcodeInfo).toBeTruthy();
expect(xcodeInfo?.stable).toBe(expected);
});
});

describe("coerce validation", () => {
beforeEach(() => {
parsePlistFileSpy = jest.spyOn(xcodeUtils, "parsePlistFile");
});

afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});

it("full version", () => {
parsePlistFileSpy.mockImplementation(() => {
return {
CFBundleShortVersionString: "12.0.1", ProductBuildVersion: "2FF"
};
});
getXcodeReleaseTypeSpy.mockImplementation(() => "GM");

const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path");
expect(xcodeInfo?.version).toBe("12.0.1");
});

it("partial version", () => {
parsePlistFileSpy.mockImplementation(() => {
return {
CFBundleShortVersionString: "10.3", ProductBuildVersion: "2FF"
};
});
getXcodeReleaseTypeSpy.mockImplementation(() => "GM");

const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path");
expect(xcodeInfo?.version).toBe("10.3.0");
});

it("invalid version", () => {
parsePlistFileSpy.mockImplementation(() => {
return {
CFBundleShortVersionString: "fake_version", ProductBuildVersion: "2FF"
};
});
getXcodeReleaseTypeSpy.mockImplementation(() => "GM");

const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path");
expect(xcodeInfo).toBeNull();
});
});
});

0 comments on commit dfa1339

Please sign in to comment.