catalyst-core 1.1.0 → 1.2.0-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/catalyst.js +54 -6
- package/changelog.md +27 -1
- package/dist/errors/registry.js +306 -18
- package/dist/errors-index.json +273 -70
- package/dist/native/androidProject/app/build.gradle.kts +53 -79
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
- package/dist/native/androidSetup.js +76 -170
- package/dist/native/androidToolchain.js +642 -0
- package/dist/native/appIcon.js +100 -0
- package/dist/native/buildAndroid/assets.js +29 -5
- package/dist/native/buildAndroid/build.js +110 -223
- package/dist/native/buildAndroid/config.js +57 -65
- package/dist/native/buildAndroid/devices.js +176 -0
- package/dist/native/buildAndroid/index.js +271 -46
- package/dist/native/buildAndroid/output.js +43 -0
- package/dist/native/buildAndroid/signing.js +456 -0
- package/dist/native/buildAppAndroid.js +27 -7
- package/dist/native/buildAppIos.js +139 -20
- package/dist/native/buildIos/assets.js +76 -4
- package/dist/native/buildIos/build.js +31 -181
- package/dist/native/buildIos/config.js +126 -11
- package/dist/native/buildIos/index.js +354 -34
- package/dist/native/buildIos/localhostCert.js +87 -0
- package/dist/native/buildIos/output.js +28 -0
- package/dist/native/buildIos/signing.js +596 -0
- package/dist/native/credentials.js +309 -0
- package/dist/native/exportCredentials.js +51 -0
- package/dist/native/iosToolchain.js +583 -0
- package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
- package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
- package/dist/native/localState.js +148 -0
- package/dist/native/nativeConfig.js +621 -0
- package/dist/native/releaseOutput.js +53 -0
- package/dist/native/renameAndroidProject.js +154 -369
- package/dist/native/setupEmulatorIos.js +58 -275
- package/dist/native/setupServer.js +520 -124
- package/dist/native/utils.js +418 -115
- package/dist/vite/resolveDevServerConfig.js +21 -0
- package/mcp_v2/knowledge-base.json +13 -11
- package/mcp_v2/test/config.test.ts +40 -0
- package/mcp_v2/tools/build.js +80 -104
- package/mcp_v2/tools/config.js +181 -174
- package/package.json +1 -1
- package/test/cli/nativeFlags.test.ts +97 -0
- package/test/native/androidDevices.test.ts +230 -0
- package/test/native/androidElfAlignment.test.ts +148 -0
- package/test/native/androidSigning.test.ts +646 -0
- package/test/native/androidToolchain.test.ts +766 -0
- package/test/native/appIcon.test.ts +124 -0
- package/test/native/credentials.test.ts +241 -0
- package/test/native/interaction.test.ts +179 -0
- package/test/native/iosConfigPhase.test.ts +611 -0
- package/test/native/iosSigning.test.ts +1060 -0
- package/test/native/iosToolchain.test.ts +821 -0
- package/test/native/localState.test.ts +80 -0
- package/test/native/nativeConfig.test.ts +602 -0
- package/test/native/setupServer.test.ts +529 -0
- package/test/native/utils.test.ts +102 -127
- package/test/vite/resolveDevServerConfig.test.js +25 -0
- package/dist/native/androidProject/keystore/Untitled +0 -0
- package/dist/native/androidProject/keystore/release-key.jks +0 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import os from "os"
|
|
4
|
+
import path from "path"
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
AVD_NAME,
|
|
9
|
+
CMDLINE_TOOLS,
|
|
10
|
+
applyJavaHome,
|
|
11
|
+
cleanSdkTemp,
|
|
12
|
+
cmdlineTool,
|
|
13
|
+
cmdlineToolsArchive,
|
|
14
|
+
defaultSdkPath,
|
|
15
|
+
ensureAndroidSdk,
|
|
16
|
+
ensureJava,
|
|
17
|
+
halfInstalledPaths,
|
|
18
|
+
installCmdlineTools,
|
|
19
|
+
installSdkPackages,
|
|
20
|
+
knownAvds,
|
|
21
|
+
missingAndroidPackages,
|
|
22
|
+
parseJavaMajor,
|
|
23
|
+
requiredAndroidPackages,
|
|
24
|
+
resolveJavaHome,
|
|
25
|
+
resolveSdkPath,
|
|
26
|
+
systemImageFor,
|
|
27
|
+
} from "../../src/native/androidToolchain.js"
|
|
28
|
+
|
|
29
|
+
const temps: string[] = []
|
|
30
|
+
function tempDir() {
|
|
31
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "catalyst-android-toolchain-"))
|
|
32
|
+
temps.push(dir)
|
|
33
|
+
return dir
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
async function rejection(promise: Promise<unknown>): Promise<any> {
|
|
41
|
+
try {
|
|
42
|
+
await promise
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return error
|
|
45
|
+
}
|
|
46
|
+
throw new Error("expected a rejection")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Mark SDK packages installed the way sdkmanager does: <sdk>/<id with ; as />/package.xml. */
|
|
50
|
+
function installPackages(sdk: string, ids: string[]) {
|
|
51
|
+
for (const id of ids) {
|
|
52
|
+
const dir = path.join(sdk, ...id.split(";"))
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
54
|
+
fs.writeFileSync(path.join(dir, "package.xml"), "<package/>")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function installTools(sdk: string) {
|
|
59
|
+
const bin = path.join(sdk, "cmdline-tools", "latest", "bin")
|
|
60
|
+
fs.mkdirSync(bin, { recursive: true })
|
|
61
|
+
for (const tool of ["sdkmanager", "avdmanager"]) fs.writeFileSync(path.join(bin, tool), "")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function acceptLicences(sdk: string) {
|
|
65
|
+
fs.mkdirSync(path.join(sdk, "licenses"), { recursive: true })
|
|
66
|
+
fs.writeFileSync(path.join(sdk, "licenses", "android-sdk-license"), "hash")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("Java resolution", () => {
|
|
70
|
+
it("parses modern and 1.x version strings", () => {
|
|
71
|
+
expect(parseJavaMajor('openjdk version "17.0.9" 2023-10-17')).toBe(17)
|
|
72
|
+
expect(parseJavaMajor('java version "1.8.0_392"')).toBe(8)
|
|
73
|
+
expect(parseJavaMajor('openjdk version "21" 2023-09-19')).toBe(21)
|
|
74
|
+
expect(parseJavaMajor("nonsense")).toBeNull()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("skips a JAVA_HOME older than 17 and falls through to java_home", () => {
|
|
78
|
+
const skipped: string[] = []
|
|
79
|
+
const run = (command: string) => {
|
|
80
|
+
if (command === "/usr/libexec/java_home") return "/jdks/zulu-17/Contents/Home\n"
|
|
81
|
+
if (command === "/old/jdk/bin/java") return 'openjdk version "11.0.2"'
|
|
82
|
+
if (command === "/jdks/zulu-17/Contents/Home/bin/java") return 'openjdk version "17.0.12"'
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
const java = resolveJavaHome({
|
|
86
|
+
env: { JAVA_HOME: "/old/jdk" },
|
|
87
|
+
platform: "darwin",
|
|
88
|
+
run,
|
|
89
|
+
exists: () => true,
|
|
90
|
+
onSkip: (candidate) => skipped.push(candidate),
|
|
91
|
+
})
|
|
92
|
+
expect(java).toEqual({ home: "/jdks/zulu-17/Contents/Home", version: 17, source: "java_home" })
|
|
93
|
+
expect(skipped[0]).toBe("/old/jdk")
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("skips a JDK newer than 23 with a reason, and prefers 21 over 17", () => {
|
|
97
|
+
const skipped: Array<[string, string]> = []
|
|
98
|
+
const versions: Record<string, string> = {
|
|
99
|
+
"/jdk25/bin/java": 'openjdk version "25" 2025-09-16',
|
|
100
|
+
"/jdk17/bin/java": 'openjdk version "17.0.12"',
|
|
101
|
+
"/jdk21/bin/java": 'openjdk version "21.0.4"',
|
|
102
|
+
}
|
|
103
|
+
const run = (command: string, args: string[]) => {
|
|
104
|
+
if (command === "/usr/libexec/java_home") {
|
|
105
|
+
return { "21": "/jdk21\n", "17": "/jdk17\n", "17+": "/jdk25\n" }[args[1]] ?? null
|
|
106
|
+
}
|
|
107
|
+
return versions[command] ?? null
|
|
108
|
+
}
|
|
109
|
+
const java = resolveJavaHome({
|
|
110
|
+
env: { JAVA_HOME: "/jdk25" },
|
|
111
|
+
platform: "darwin",
|
|
112
|
+
run,
|
|
113
|
+
exists: (file) => file in versions,
|
|
114
|
+
onSkip: (candidate, reason) => skipped.push([candidate, reason]),
|
|
115
|
+
})
|
|
116
|
+
expect(java).toEqual({ home: "/jdk21", version: 21, source: "java_home" })
|
|
117
|
+
expect(skipped).toEqual([["/jdk25", expect.stringContaining("newer than Gradle 8.13 supports")]])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("takes 17 over another in-range JDK, and nothing when only 24+ is installed", () => {
|
|
121
|
+
const versions: Record<string, string> = {
|
|
122
|
+
"/jdk22/bin/java": 'openjdk version "22.0.1"',
|
|
123
|
+
"/jdk17/bin/java": 'openjdk version "17.0.12"',
|
|
124
|
+
}
|
|
125
|
+
const pick = (javaHome: string, others: string[]) =>
|
|
126
|
+
resolveJavaHome({
|
|
127
|
+
env: { JAVA_HOME: javaHome },
|
|
128
|
+
platform: "linux",
|
|
129
|
+
run: (command: string) =>
|
|
130
|
+
command === "sh" ? `${others[0] || ""}/bin/java` : (versions[command] ?? null),
|
|
131
|
+
exists: (file) => file in versions,
|
|
132
|
+
})
|
|
133
|
+
expect(pick("/jdk22", ["/jdk17"])).toMatchObject({ home: "/jdk17", version: 17 })
|
|
134
|
+
expect(
|
|
135
|
+
resolveJavaHome({
|
|
136
|
+
env: { JAVA_HOME: "/jdk24" },
|
|
137
|
+
platform: "linux",
|
|
138
|
+
run: () => 'openjdk version "24"',
|
|
139
|
+
exists: (file) => file === "/jdk24/bin/java",
|
|
140
|
+
})
|
|
141
|
+
).toBeNull()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("uses Android Studio's bundled JBR when nothing else is there", () => {
|
|
145
|
+
const jbr = "/Applications/Android Studio.app/Contents/jbr/Contents/Home"
|
|
146
|
+
const java = resolveJavaHome({
|
|
147
|
+
env: {},
|
|
148
|
+
platform: "darwin",
|
|
149
|
+
run: (command) => (command === `${jbr}/bin/java` ? 'openjdk version "21.0.6"' : null),
|
|
150
|
+
exists: (file) => file === `${jbr}/bin/java`,
|
|
151
|
+
})
|
|
152
|
+
expect(java).toMatchObject({ home: jbr, version: 21, source: "Android Studio" })
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("returns null with no JDK 17+", () => {
|
|
156
|
+
expect(
|
|
157
|
+
resolveJavaHome({ env: {}, platform: "darwin", run: () => null, exists: () => false })
|
|
158
|
+
).toBeNull()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it("points JAVA_HOME and PATH at the JDK without duplicating it", () => {
|
|
162
|
+
const env: Record<string, string> = { PATH: ["/usr/bin", "/bin"].join(path.delimiter) }
|
|
163
|
+
applyJavaHome({ home: "/jdk", version: 17, source: "JAVA_HOME" }, env)
|
|
164
|
+
applyJavaHome({ home: "/jdk", version: 17, source: "JAVA_HOME" }, env)
|
|
165
|
+
expect(env.JAVA_HOME).toBe("/jdk")
|
|
166
|
+
expect(env.PATH.split(path.delimiter)).toEqual([path.join("/jdk", "bin"), "/usr/bin", "/bin"])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it("fails with NATIVE-005 naming --yes without a terminal, and never installs", async () => {
|
|
170
|
+
const installs: string[][] = []
|
|
171
|
+
const error = await rejection(
|
|
172
|
+
ensureJava({
|
|
173
|
+
platform: "darwin",
|
|
174
|
+
isTTY: false,
|
|
175
|
+
hasCommand: () => true,
|
|
176
|
+
resolve: () => null,
|
|
177
|
+
runInherited: async (_command: string, args: string[]) => void installs.push(args),
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
expect(error.code).toBe("NATIVE-005")
|
|
181
|
+
expect(error.message).toContain("--yes")
|
|
182
|
+
expect(installs).toEqual([])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("installs with --yes and no terminal", async () => {
|
|
186
|
+
let installed = false
|
|
187
|
+
const java = await ensureJava({
|
|
188
|
+
platform: "darwin",
|
|
189
|
+
flags: { yes: true },
|
|
190
|
+
isTTY: false,
|
|
191
|
+
hasCommand: () => true,
|
|
192
|
+
resolve: () => (installed ? { home: "/zulu", version: 17, source: "java_home" } : null),
|
|
193
|
+
runInherited: async () => {
|
|
194
|
+
installed = true
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
expect(java.home).toBe("/zulu")
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("fails with ANDROID-007 when the install is declined or Homebrew is missing", async () => {
|
|
201
|
+
const declined = await rejection(
|
|
202
|
+
ensureJava({
|
|
203
|
+
platform: "darwin",
|
|
204
|
+
isTTY: true,
|
|
205
|
+
hasCommand: () => true,
|
|
206
|
+
ask: async () => false,
|
|
207
|
+
resolve: () => null,
|
|
208
|
+
})
|
|
209
|
+
)
|
|
210
|
+
expect(declined.code).toBe("ANDROID-007")
|
|
211
|
+
const noBrew = await rejection(
|
|
212
|
+
ensureJava({ platform: "darwin", isTTY: true, hasCommand: () => false, resolve: () => null })
|
|
213
|
+
)
|
|
214
|
+
expect(noBrew.code).toBe("ANDROID-007")
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it("fails with ANDROID-007 on Linux, naming a package to install", async () => {
|
|
218
|
+
const error = await rejection(ensureJava({ platform: "linux", isTTY: true, resolve: () => null }))
|
|
219
|
+
expect(error.code).toBe("ANDROID-007")
|
|
220
|
+
expect(error.message).toContain("openjdk-17-jdk")
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it("installs Zulu 17 with Homebrew after a terminal yes", async () => {
|
|
224
|
+
let installed = false
|
|
225
|
+
const calls: string[][] = []
|
|
226
|
+
const java = await ensureJava({
|
|
227
|
+
platform: "darwin",
|
|
228
|
+
isTTY: true,
|
|
229
|
+
hasCommand: () => true,
|
|
230
|
+
ask: async () => true,
|
|
231
|
+
resolve: () => (installed ? { home: "/zulu", version: 17, source: "java_home" } : null),
|
|
232
|
+
runInherited: async (command: string, args: string[]) => {
|
|
233
|
+
calls.push([command, ...args])
|
|
234
|
+
installed = true
|
|
235
|
+
},
|
|
236
|
+
})
|
|
237
|
+
expect(calls).toEqual([["brew", "install", "--cask", "zulu@17"]])
|
|
238
|
+
expect(java.home).toBe("/zulu")
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe("SDK packages", () => {
|
|
243
|
+
it("picks the system image ABI from the CPU", () => {
|
|
244
|
+
expect(systemImageFor("arm64")).toBe("system-images;android-36;google_apis;arm64-v8a")
|
|
245
|
+
expect(systemImageFor("x64")).toBe("system-images;android-36;google_apis;x86_64")
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it("needs the system image only when an emulator is to be created", () => {
|
|
249
|
+
expect(requiredAndroidPackages({ arch: "arm64" })).toEqual([
|
|
250
|
+
"platform-tools",
|
|
251
|
+
"emulator",
|
|
252
|
+
"platforms;android-36",
|
|
253
|
+
"build-tools;36.0.0",
|
|
254
|
+
])
|
|
255
|
+
expect(requiredAndroidPackages({ arch: "x64", withSystemImage: true })).toContain(
|
|
256
|
+
"system-images;android-36;google_apis;x86_64"
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it("leaves the emulator out for a release or a connected phone", () => {
|
|
261
|
+
expect(requiredAndroidPackages({ arch: "arm64", withEmulator: false })).toEqual([
|
|
262
|
+
"platform-tools",
|
|
263
|
+
"platforms;android-36",
|
|
264
|
+
"build-tools;36.0.0",
|
|
265
|
+
])
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it("reads the SDK path from config, then the environment, and never prompts", () => {
|
|
269
|
+
expect(resolveSdkPath({ sdkPath: "/custom/sdk" }, {})).toBe("/custom/sdk")
|
|
270
|
+
expect(resolveSdkPath({ sdkPath: "~/sdk" }, {})).toBe(path.join(os.homedir(), "sdk"))
|
|
271
|
+
const home = tempDir()
|
|
272
|
+
fs.mkdirSync(path.join(home, "platform-tools"))
|
|
273
|
+
fs.writeFileSync(path.join(home, "platform-tools", "adb"), "")
|
|
274
|
+
expect(resolveSdkPath({}, { ANDROID_HOME: home })).toBe(home)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it("finds what is missing from package.xml files", () => {
|
|
278
|
+
const sdk = tempDir()
|
|
279
|
+
installPackages(sdk, ["platform-tools", "platforms;android-36"])
|
|
280
|
+
expect(missingAndroidPackages(sdk, requiredAndroidPackages())).toEqual([
|
|
281
|
+
"emulator",
|
|
282
|
+
"build-tools;36.0.0",
|
|
283
|
+
])
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it("pins an archive with a SHA-256 for each host", () => {
|
|
287
|
+
expect(cmdlineToolsArchive("darwin", "arm64").url).toBe(
|
|
288
|
+
`https://dl.google.com/android/repository/commandlinetools-mac_arm64-${CMDLINE_TOOLS.build}_latest.zip`
|
|
289
|
+
)
|
|
290
|
+
expect(cmdlineToolsArchive("darwin", "x64").file).toContain("mac_x86_64")
|
|
291
|
+
expect(cmdlineToolsArchive("linux", "x64").file).toContain("linux")
|
|
292
|
+
for (const archive of Object.values(CMDLINE_TOOLS.archives))
|
|
293
|
+
expect(archive.sha256).toMatch(/^[0-9a-f]{64}$/)
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe("installCmdlineTools", () => {
|
|
298
|
+
const payload = Buffer.from("pretend zip")
|
|
299
|
+
const archive = {
|
|
300
|
+
file: "commandlinetools-test.zip",
|
|
301
|
+
url: "https://example.invalid/tools.zip",
|
|
302
|
+
sha256: crypto.createHash("sha256").update(payload).digest("hex"),
|
|
303
|
+
}
|
|
304
|
+
const download = async (_url: string, dest: string) => fs.writeFileSync(dest, payload)
|
|
305
|
+
const unzip = (_zip: string, dest: string) => {
|
|
306
|
+
fs.mkdirSync(path.join(dest, "cmdline-tools", "bin"), { recursive: true })
|
|
307
|
+
fs.writeFileSync(path.join(dest, "cmdline-tools", "bin", "sdkmanager"), "")
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
it("verifies the checksum and unpacks into cmdline-tools/latest", async () => {
|
|
311
|
+
const sdk = tempDir()
|
|
312
|
+
const latest = await installCmdlineTools(sdk, { archive, download, unzip })
|
|
313
|
+
expect(latest).toBe(path.join(sdk, "cmdline-tools", "latest"))
|
|
314
|
+
expect(fs.existsSync(path.join(latest, "bin", "sdkmanager"))).toBe(true)
|
|
315
|
+
// No staging folders are left behind.
|
|
316
|
+
expect(fs.readdirSync(path.join(sdk, "cmdline-tools"))).toEqual(["latest"])
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it("fails with ANDROID-009 on a checksum mismatch and unpacks nothing", async () => {
|
|
320
|
+
const sdk = tempDir()
|
|
321
|
+
const error = await rejection(
|
|
322
|
+
installCmdlineTools(sdk, { archive: { ...archive, sha256: "0".repeat(64) }, download, unzip })
|
|
323
|
+
)
|
|
324
|
+
expect(error.code).toBe("ANDROID-009")
|
|
325
|
+
expect(error.message).toContain("SHA-256")
|
|
326
|
+
expect(fs.existsSync(path.join(sdk, "cmdline-tools", "latest"))).toBe(false)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it("fails with ANDROID-009 when the archive does not unpack, or has no cmdline-tools folder", async () => {
|
|
330
|
+
const sdk = tempDir()
|
|
331
|
+
const broken = await rejection(
|
|
332
|
+
installCmdlineTools(sdk, {
|
|
333
|
+
archive,
|
|
334
|
+
download,
|
|
335
|
+
unzip: () => {
|
|
336
|
+
throw Object.assign(new Error("Command failed: unzip"), {
|
|
337
|
+
stderr: "End-of-central-directory signature not found",
|
|
338
|
+
})
|
|
339
|
+
},
|
|
340
|
+
})
|
|
341
|
+
)
|
|
342
|
+
expect(broken.code).toBe("ANDROID-009")
|
|
343
|
+
expect(broken.message).toContain("could not unpack commandlinetools-test.zip")
|
|
344
|
+
expect(broken.message).toContain("End-of-central-directory")
|
|
345
|
+
|
|
346
|
+
const empty = await rejection(installCmdlineTools(sdk, { archive, download, unzip: () => {} }))
|
|
347
|
+
expect(empty.code).toBe("ANDROID-009")
|
|
348
|
+
expect(empty.message).toContain("has no cmdline-tools folder")
|
|
349
|
+
// Neither failure leaves a staging folder or a half-made latest/.
|
|
350
|
+
expect(fs.readdirSync(path.join(sdk, "cmdline-tools"))).toEqual([])
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it("fails with ANDROID-009 when the download fails", async () => {
|
|
354
|
+
const error = await rejection(
|
|
355
|
+
installCmdlineTools(tempDir(), {
|
|
356
|
+
archive,
|
|
357
|
+
download: async () => {
|
|
358
|
+
throw Object.assign(new Error("Command failed with exit code 6"), {
|
|
359
|
+
output: "curl: (6) Could not resolve host",
|
|
360
|
+
})
|
|
361
|
+
},
|
|
362
|
+
unzip,
|
|
363
|
+
})
|
|
364
|
+
)
|
|
365
|
+
expect(error.code).toBe("ANDROID-009")
|
|
366
|
+
expect(error.message).toContain("Could not resolve host")
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
describe("SDK locations", () => {
|
|
371
|
+
it("puts a new SDK at ANDROID_HOME, ANDROID_SDK_ROOT, or the platform default", () => {
|
|
372
|
+
expect(defaultSdkPath({ ANDROID_HOME: "/a", ANDROID_SDK_ROOT: "/b" }, "linux")).toBe("/a")
|
|
373
|
+
expect(defaultSdkPath({ ANDROID_SDK_ROOT: "/b" }, "linux")).toBe("/b")
|
|
374
|
+
expect(defaultSdkPath({}, "darwin")).toBe(path.join(os.homedir(), "Library", "Android", "sdk"))
|
|
375
|
+
expect(defaultSdkPath({}, "linux")).toBe(path.join(os.homedir(), "Android", "Sdk"))
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it("finds sdkmanager in cmdline-tools/latest, else the newest versioned folder, else nothing", () => {
|
|
379
|
+
const sdk = tempDir()
|
|
380
|
+
expect(cmdlineTool(sdk, "sdkmanager", { platform: "linux" })).toBeNull()
|
|
381
|
+
for (const version of ["9.0", "11.0"]) {
|
|
382
|
+
fs.mkdirSync(path.join(sdk, "cmdline-tools", version, "bin"), { recursive: true })
|
|
383
|
+
}
|
|
384
|
+
expect(cmdlineTool(sdk, "sdkmanager", { platform: "linux" })).toBeNull()
|
|
385
|
+
fs.writeFileSync(path.join(sdk, "cmdline-tools", "9.0", "bin", "sdkmanager"), "")
|
|
386
|
+
fs.writeFileSync(path.join(sdk, "cmdline-tools", "11.0", "bin", "sdkmanager.bat"), "")
|
|
387
|
+
expect(cmdlineTool(sdk, "sdkmanager", { platform: "linux" })).toBe(
|
|
388
|
+
path.join(sdk, "cmdline-tools", "9.0", "bin", "sdkmanager")
|
|
389
|
+
)
|
|
390
|
+
expect(cmdlineTool(sdk, "sdkmanager", { platform: "win32" })).toBe(
|
|
391
|
+
path.join(sdk, "cmdline-tools", "11.0", "bin", "sdkmanager.bat")
|
|
392
|
+
)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it("knows AVDs from the AVD folder when the emulator is not installed yet", () => {
|
|
396
|
+
const avdHome = tempDir()
|
|
397
|
+
fs.writeFileSync(path.join(avdHome, "Pixel_8.ini"), "")
|
|
398
|
+
fs.mkdirSync(path.join(avdHome, "Pixel_8.avd"))
|
|
399
|
+
fs.writeFileSync(path.join(avdHome, "notes.txt"), "")
|
|
400
|
+
// The SDK has no emulator binary, so only the folder answers.
|
|
401
|
+
const sdk = tempDir()
|
|
402
|
+
expect(knownAvds(sdk, { env: { ANDROID_AVD_HOME: avdHome } })).toEqual(["Pixel_8"])
|
|
403
|
+
|
|
404
|
+
const userHome = tempDir()
|
|
405
|
+
fs.mkdirSync(path.join(userHome, "avd"))
|
|
406
|
+
fs.writeFileSync(path.join(userHome, "avd", "Tablet.ini"), "")
|
|
407
|
+
expect(knownAvds(sdk, { env: { ANDROID_USER_HOME: userHome } })).toEqual(["Tablet"])
|
|
408
|
+
|
|
409
|
+
expect(knownAvds(sdk, { env: { ANDROID_AVD_HOME: path.join(avdHome, "missing") } })).toEqual([])
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
describe("ensureAndroidSdk", () => {
|
|
414
|
+
const base = ["platform-tools", "emulator", "platforms;android-36", "build-tools;36.0.0"]
|
|
415
|
+
const image = systemImageFor("arm64")
|
|
416
|
+
|
|
417
|
+
function fakes(sdk: string) {
|
|
418
|
+
const calls: Array<{ how: string; command: string; args: string[]; responses?: object }> = []
|
|
419
|
+
const deps = {
|
|
420
|
+
run: async (command: string, args: string[], responses?: object) => {
|
|
421
|
+
calls.push({ how: "captured", command: path.basename(command), args, responses })
|
|
422
|
+
if (args.includes("--licenses")) acceptLicences(sdk)
|
|
423
|
+
const ids = args.filter((arg) => !arg.startsWith("-") && arg !== "create" && arg !== "avd")
|
|
424
|
+
if (path.basename(command) === "sdkmanager") installPackages(sdk, ids)
|
|
425
|
+
return ""
|
|
426
|
+
},
|
|
427
|
+
runInherited: async (command: string, args: string[]) => {
|
|
428
|
+
calls.push({ how: "inherited", command: path.basename(command), args })
|
|
429
|
+
if (args.includes("--licenses")) acceptLicences(sdk)
|
|
430
|
+
else
|
|
431
|
+
installPackages(
|
|
432
|
+
sdk,
|
|
433
|
+
args.filter((arg) => !arg.startsWith("-"))
|
|
434
|
+
)
|
|
435
|
+
},
|
|
436
|
+
installTools: async (dir: string) => installTools(dir),
|
|
437
|
+
}
|
|
438
|
+
return { calls, deps }
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
it("does nothing when everything is installed and the AVD exists", async () => {
|
|
442
|
+
const sdk = tempDir()
|
|
443
|
+
installPackages(sdk, base)
|
|
444
|
+
const { calls, deps } = fakes(sdk)
|
|
445
|
+
const result = await ensureAndroidSdk({
|
|
446
|
+
sdkPath: sdk,
|
|
447
|
+
emulatorName: "Pixel_7",
|
|
448
|
+
isTTY: false,
|
|
449
|
+
arch: "arm64",
|
|
450
|
+
...deps,
|
|
451
|
+
listAvds: () => ["Pixel_7"],
|
|
452
|
+
})
|
|
453
|
+
expect(result).toEqual({ sdkPath: sdk, emulatorName: "Pixel_7", installed: [], createdAvd: false })
|
|
454
|
+
expect(calls).toEqual([])
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it("reuses an existing AVD instead of downloading an image when none is configured", async () => {
|
|
458
|
+
const sdk = tempDir()
|
|
459
|
+
installPackages(sdk, base)
|
|
460
|
+
const result = await ensureAndroidSdk({
|
|
461
|
+
sdkPath: sdk,
|
|
462
|
+
isTTY: false,
|
|
463
|
+
arch: "arm64",
|
|
464
|
+
listAvds: () => ["Medium_Phone"],
|
|
465
|
+
...fakes(sdk).deps,
|
|
466
|
+
})
|
|
467
|
+
expect(result.emulatorName).toBe("Medium_Phone")
|
|
468
|
+
expect(result.createdAvd).toBe(false)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it("fails with NATIVE-005 without a terminal or --yes, listing what it would fetch", async () => {
|
|
472
|
+
const sdk = tempDir()
|
|
473
|
+
const { calls, deps } = fakes(sdk)
|
|
474
|
+
const error = await rejection(
|
|
475
|
+
ensureAndroidSdk({ sdkPath: sdk, isTTY: false, arch: "arm64", listAvds: () => [], ...deps })
|
|
476
|
+
)
|
|
477
|
+
expect(error.code).toBe("NATIVE-005")
|
|
478
|
+
expect(error.message).toContain("Android command-line tools")
|
|
479
|
+
expect(error.message).toContain(image)
|
|
480
|
+
expect(error.message).toContain("--yes")
|
|
481
|
+
expect(calls).toEqual([])
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it("fails with NATIVE-005 naming --accept-licenses when only --yes is given in CI", async () => {
|
|
485
|
+
const sdk = tempDir()
|
|
486
|
+
const error = await rejection(
|
|
487
|
+
ensureAndroidSdk({
|
|
488
|
+
sdkPath: sdk,
|
|
489
|
+
flags: { yes: true },
|
|
490
|
+
isTTY: false,
|
|
491
|
+
arch: "arm64",
|
|
492
|
+
listAvds: () => [],
|
|
493
|
+
...fakes(sdk).deps,
|
|
494
|
+
})
|
|
495
|
+
)
|
|
496
|
+
expect(error.code).toBe("NATIVE-005")
|
|
497
|
+
expect(error.message).toContain("--accept-licenses")
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it("installs from nothing in CI with --yes --accept-licenses and creates the AVD", async () => {
|
|
501
|
+
const sdk = tempDir()
|
|
502
|
+
const { calls, deps } = fakes(sdk)
|
|
503
|
+
const result = await ensureAndroidSdk({
|
|
504
|
+
sdkPath: sdk,
|
|
505
|
+
flags: { yes: true, acceptLicenses: true },
|
|
506
|
+
isTTY: false,
|
|
507
|
+
arch: "arm64",
|
|
508
|
+
listAvds: () => [],
|
|
509
|
+
...deps,
|
|
510
|
+
})
|
|
511
|
+
expect(result).toMatchObject({
|
|
512
|
+
emulatorName: AVD_NAME,
|
|
513
|
+
createdAvd: true,
|
|
514
|
+
installed: [...base, image],
|
|
515
|
+
})
|
|
516
|
+
expect(
|
|
517
|
+
calls.map((call) => [
|
|
518
|
+
call.how,
|
|
519
|
+
call.command,
|
|
520
|
+
call.args.filter((a) => !a.startsWith("--sdk_root")),
|
|
521
|
+
])
|
|
522
|
+
).toEqual([
|
|
523
|
+
["captured", "sdkmanager", ["--licenses"]],
|
|
524
|
+
["captured", "sdkmanager", [...base, image]],
|
|
525
|
+
["captured", "avdmanager", ["create", "avd", "-n", AVD_NAME, "-k", image, "-d", "pixel_7"]],
|
|
526
|
+
])
|
|
527
|
+
// Licences were accepted by flag, so sdkmanager may answer yes for later ones too.
|
|
528
|
+
expect(calls[1].responses).toEqual({ "(y/N)": "y" })
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it("asks once in a terminal, then runs licences and installs on the user's terminal", async () => {
|
|
532
|
+
const sdk = tempDir()
|
|
533
|
+
installPackages(sdk, ["platform-tools", "emulator", "platforms;android-36"])
|
|
534
|
+
installTools(sdk)
|
|
535
|
+
const questions: string[] = []
|
|
536
|
+
const { calls, deps } = fakes(sdk)
|
|
537
|
+
const result = await ensureAndroidSdk({
|
|
538
|
+
sdkPath: sdk,
|
|
539
|
+
emulatorName: "Pixel_7",
|
|
540
|
+
isTTY: true,
|
|
541
|
+
arch: "arm64",
|
|
542
|
+
ask: async (kind: string) => {
|
|
543
|
+
questions.push(kind)
|
|
544
|
+
return true
|
|
545
|
+
},
|
|
546
|
+
listAvds: () => ["Pixel_7"],
|
|
547
|
+
...deps,
|
|
548
|
+
})
|
|
549
|
+
// One question for the download, one for the licences sdkmanager then shows.
|
|
550
|
+
expect(questions).toEqual(["install", "licence"])
|
|
551
|
+
expect(result).toMatchObject({
|
|
552
|
+
emulatorName: "Pixel_7",
|
|
553
|
+
installed: ["build-tools;36.0.0"],
|
|
554
|
+
createdAvd: false,
|
|
555
|
+
})
|
|
556
|
+
expect(calls.map((call) => call.how)).toEqual(["inherited", "inherited"])
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it("fails with ANDROID-008 when the terminal user declines", async () => {
|
|
560
|
+
const sdk = tempDir()
|
|
561
|
+
const error = await rejection(
|
|
562
|
+
ensureAndroidSdk({
|
|
563
|
+
sdkPath: sdk,
|
|
564
|
+
isTTY: true,
|
|
565
|
+
arch: "arm64",
|
|
566
|
+
ask: async () => false,
|
|
567
|
+
listAvds: () => [],
|
|
568
|
+
...fakes(sdk).deps,
|
|
569
|
+
})
|
|
570
|
+
)
|
|
571
|
+
expect(error.code).toBe("ANDROID-008")
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it("needs no emulator or AVD for a release or a connected phone", async () => {
|
|
575
|
+
const sdk = tempDir()
|
|
576
|
+
installPackages(sdk, ["platform-tools", "platforms;android-36", "build-tools;36.0.0"])
|
|
577
|
+
const { calls, deps } = fakes(sdk)
|
|
578
|
+
const result = await ensureAndroidSdk({
|
|
579
|
+
sdkPath: sdk,
|
|
580
|
+
needEmulator: false,
|
|
581
|
+
isTTY: false,
|
|
582
|
+
arch: "arm64",
|
|
583
|
+
...deps,
|
|
584
|
+
listAvds: () => [],
|
|
585
|
+
})
|
|
586
|
+
expect(result).toEqual({ sdkPath: sdk, emulatorName: null, installed: [], createdAvd: false })
|
|
587
|
+
expect(calls).toEqual([])
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
it("reuses the last AVD before any other, without asking", async () => {
|
|
591
|
+
const sdk = tempDir()
|
|
592
|
+
installPackages(sdk, base)
|
|
593
|
+
const result = await ensureAndroidSdk({
|
|
594
|
+
sdkPath: sdk,
|
|
595
|
+
preferredAvd: "Tablet",
|
|
596
|
+
isTTY: true,
|
|
597
|
+
arch: "arm64",
|
|
598
|
+
ask: async () => {
|
|
599
|
+
throw new Error("should not ask")
|
|
600
|
+
},
|
|
601
|
+
listAvds: () => ["Phone", "Tablet"],
|
|
602
|
+
...fakes(sdk).deps,
|
|
603
|
+
})
|
|
604
|
+
expect(result.emulatorName).toBe("Tablet")
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
it("replaces a configured AVD that does not exist with catalyst-api36", async () => {
|
|
608
|
+
const sdk = tempDir()
|
|
609
|
+
installPackages(sdk, [...base, image])
|
|
610
|
+
installTools(sdk)
|
|
611
|
+
const logs: string[] = []
|
|
612
|
+
const result = await ensureAndroidSdk({
|
|
613
|
+
sdkPath: sdk,
|
|
614
|
+
emulatorName: "Gone_Phone",
|
|
615
|
+
flags: { yes: true },
|
|
616
|
+
isTTY: false,
|
|
617
|
+
arch: "arm64",
|
|
618
|
+
listAvds: () => [],
|
|
619
|
+
log: (message: string) => logs.push(message),
|
|
620
|
+
...fakes(sdk).deps,
|
|
621
|
+
})
|
|
622
|
+
expect(result).toMatchObject({ emulatorName: AVD_NAME, createdAvd: true, installed: [] })
|
|
623
|
+
expect(logs.some((line) => line.includes('"Gone_Phone" does not exist'))).toBe(true)
|
|
624
|
+
})
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
describe("ensureAndroidSdk failures after consent", () => {
|
|
628
|
+
const ci = { flags: { yes: true, acceptLicenses: true }, isTTY: false, arch: "arm64" }
|
|
629
|
+
|
|
630
|
+
it("fails with ANDROID-009 when the tools install leaves no sdkmanager", async () => {
|
|
631
|
+
const sdk = tempDir()
|
|
632
|
+
const error = await rejection(
|
|
633
|
+
ensureAndroidSdk({
|
|
634
|
+
sdkPath: sdk,
|
|
635
|
+
...ci,
|
|
636
|
+
listAvds: () => [],
|
|
637
|
+
installTools: async () => {},
|
|
638
|
+
run: async () => "",
|
|
639
|
+
})
|
|
640
|
+
)
|
|
641
|
+
expect(error.code).toBe("ANDROID-009")
|
|
642
|
+
expect(error.message).toContain("no sdkmanager under")
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it("fails with ANDROID-008 when sdkmanager --licenses does not record the licence", async () => {
|
|
646
|
+
const sdk = tempDir()
|
|
647
|
+
installTools(sdk)
|
|
648
|
+
const ran: string[][] = []
|
|
649
|
+
const error = await rejection(
|
|
650
|
+
ensureAndroidSdk({
|
|
651
|
+
sdkPath: sdk,
|
|
652
|
+
...ci,
|
|
653
|
+
listAvds: () => ["Pixel_7"],
|
|
654
|
+
emulatorName: "Pixel_7",
|
|
655
|
+
run: async (_command: string, args: string[]) => {
|
|
656
|
+
ran.push(args)
|
|
657
|
+
return ""
|
|
658
|
+
},
|
|
659
|
+
})
|
|
660
|
+
)
|
|
661
|
+
expect(error.code).toBe("ANDROID-008")
|
|
662
|
+
expect(error.message).toContain("licences were not accepted")
|
|
663
|
+
expect(ran).toHaveLength(1)
|
|
664
|
+
expect(ran[0]).toContain("--licenses")
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
it("fails with ANDROID-009 when an AVD is needed and there is no avdmanager", async () => {
|
|
668
|
+
const sdk = tempDir()
|
|
669
|
+
const bin = path.join(sdk, "cmdline-tools", "latest", "bin")
|
|
670
|
+
fs.mkdirSync(bin, { recursive: true })
|
|
671
|
+
fs.writeFileSync(path.join(bin, "sdkmanager"), "")
|
|
672
|
+
acceptLicences(sdk)
|
|
673
|
+
const error = await rejection(
|
|
674
|
+
ensureAndroidSdk({
|
|
675
|
+
sdkPath: sdk,
|
|
676
|
+
...ci,
|
|
677
|
+
listAvds: () => [],
|
|
678
|
+
run: async (command: string, args: string[]) => {
|
|
679
|
+
if (path.basename(command) === "sdkmanager") {
|
|
680
|
+
installPackages(
|
|
681
|
+
sdk,
|
|
682
|
+
args.filter((arg) => !arg.startsWith("-"))
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
return ""
|
|
686
|
+
},
|
|
687
|
+
})
|
|
688
|
+
)
|
|
689
|
+
expect(error.code).toBe("ANDROID-009")
|
|
690
|
+
expect(error.message).toContain("no avdmanager under")
|
|
691
|
+
})
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
describe("sdkmanager recovery (#301)", () => {
|
|
695
|
+
it("removes stale partial downloads under <sdk>/.temp", () => {
|
|
696
|
+
const sdk = tempDir()
|
|
697
|
+
fs.mkdirSync(path.join(sdk, ".temp", "PackageOperation01"), { recursive: true })
|
|
698
|
+
fs.writeFileSync(path.join(sdk, ".temp", "PackageOperation01", "emulator.zip"), "partial")
|
|
699
|
+
const logs: string[] = []
|
|
700
|
+
expect(cleanSdkTemp(sdk, { log: (message: string) => logs.push(message) })).toEqual([
|
|
701
|
+
"PackageOperation01",
|
|
702
|
+
])
|
|
703
|
+
expect(fs.readdirSync(path.join(sdk, ".temp"))).toEqual([])
|
|
704
|
+
expect(logs).toHaveLength(1)
|
|
705
|
+
expect(cleanSdkTemp(tempDir())).toEqual([])
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
it("finds half-installed folders, and only package folders inside the SDK", () => {
|
|
709
|
+
const sdk = tempDir()
|
|
710
|
+
installPackages(sdk, ["platforms;android-36"])
|
|
711
|
+
fs.mkdirSync(path.join(sdk, "emulator"))
|
|
712
|
+
const output = [
|
|
713
|
+
`Warning: java.nio.file.FileAlreadyExistsException: ${path.join(sdk, "build-tools", "36.0.0")}`,
|
|
714
|
+
`java.nio.file.FileAlreadyExistsException: ${path.join(sdk, "platforms")}`,
|
|
715
|
+
"java.nio.file.FileAlreadyExistsException: /etc",
|
|
716
|
+
].join("\n")
|
|
717
|
+
const ids = ["emulator", "platforms;android-36", "build-tools;36.0.0"]
|
|
718
|
+
expect(halfInstalledPaths(sdk, ids, output).sort()).toEqual(
|
|
719
|
+
[path.join(sdk, "build-tools", "36.0.0"), path.join(sdk, "emulator")].sort()
|
|
720
|
+
)
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
it("cleans a half-installed package after FileAlreadyExistsException and retries once", async () => {
|
|
724
|
+
const sdk = tempDir()
|
|
725
|
+
let runs = 0
|
|
726
|
+
await installSdkPackages(sdk, ["emulator"], async () => {
|
|
727
|
+
runs++
|
|
728
|
+
fs.mkdirSync(path.join(sdk, ".temp", `op${runs}`), { recursive: true })
|
|
729
|
+
if (runs === 1) {
|
|
730
|
+
fs.mkdirSync(path.join(sdk, "emulator", "lib"), { recursive: true })
|
|
731
|
+
throw Object.assign(new Error("exit 1"), {
|
|
732
|
+
output: `java.nio.file.FileAlreadyExistsException: ${path.join(sdk, "emulator", "lib")}`,
|
|
733
|
+
})
|
|
734
|
+
}
|
|
735
|
+
expect(fs.existsSync(path.join(sdk, "emulator"))).toBe(false)
|
|
736
|
+
installPackages(sdk, ["emulator"])
|
|
737
|
+
return ""
|
|
738
|
+
})
|
|
739
|
+
expect(runs).toBe(2)
|
|
740
|
+
expect(fs.readdirSync(path.join(sdk, ".temp"))).toEqual(["op2"])
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
it("retries only once, and not at all for an unrelated failure", async () => {
|
|
744
|
+
const sdk = tempDir()
|
|
745
|
+
let runs = 0
|
|
746
|
+
const stuck = await rejection(
|
|
747
|
+
installSdkPackages(sdk, ["emulator"], async () => {
|
|
748
|
+
runs++
|
|
749
|
+
fs.mkdirSync(path.join(sdk, "emulator"), { recursive: true })
|
|
750
|
+
return "Warning: java.nio.file.FileAlreadyExistsException"
|
|
751
|
+
})
|
|
752
|
+
)
|
|
753
|
+
expect(runs).toBe(2)
|
|
754
|
+
expect(stuck.message).toContain("did not install emulator")
|
|
755
|
+
|
|
756
|
+
runs = 0
|
|
757
|
+
const network = await rejection(
|
|
758
|
+
installSdkPackages(tempDir(), ["emulator"], async () => {
|
|
759
|
+
runs++
|
|
760
|
+
throw Object.assign(new Error("exit 1"), { output: "Failed to download" })
|
|
761
|
+
})
|
|
762
|
+
)
|
|
763
|
+
expect(runs).toBe(1)
|
|
764
|
+
expect(network.message).toBe("exit 1")
|
|
765
|
+
})
|
|
766
|
+
})
|