@devicecloud.dev/dcd 0.0.3 → 0.0.4
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/dist/commands/cloud.d.ts +17 -0
- package/dist/commands/cloud.js +49 -231
- package/dist/constants.d.ts +23 -0
- package/dist/constants.js +121 -0
- package/dist/methods.d.ts +15 -0
- package/dist/methods.js +100 -0
- package/dist/plan.d.ts +3 -0
- package/dist/plan.js +76 -69
- package/dist/planMethods.d.ts +17 -0
- package/dist/planMethods.js +109 -0
- package/oclif.manifest.json +1 -1
- package/package.json +4 -3
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
export declare enum EiOSDevices {
|
|
3
|
+
'ipad-pro-6th-gen' = "ipad-pro-6th-gen",
|
|
4
|
+
'iphone-12' = "iphone-12",
|
|
5
|
+
'iphone-12-mini' = "iphone-12-mini",
|
|
6
|
+
'iphone-12-pro-max' = "iphone-12-pro-max",
|
|
7
|
+
'iphone-13' = "iphone-13",
|
|
8
|
+
'iphone-13-mini' = "iphone-13-mini",
|
|
9
|
+
'iphone-13-pro-max' = "iphone-13-pro-max",
|
|
10
|
+
'iphone-14' = "iphone-14",
|
|
11
|
+
'iphone-14-plus' = "iphone-14-plus",
|
|
12
|
+
'iphone-14-pro' = "iphone-14-pro",
|
|
13
|
+
'iphone-14-pro-max' = "iphone-14-pro-max",
|
|
14
|
+
'iphone-15' = "iphone-15",
|
|
15
|
+
'iphone-15-plus' = "iphone-15-plus",
|
|
16
|
+
'iphone-15-pro' = "iphone-15-pro",
|
|
17
|
+
'iphone-15-pro-max' = "iphone-15-pro-max"
|
|
18
|
+
}
|
|
2
19
|
export default class Cloud extends Command {
|
|
3
20
|
static args: {
|
|
4
21
|
firstFile: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -1,91 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EiOSDevices = void 0;
|
|
3
4
|
/* eslint-disable complexity */
|
|
4
5
|
const core_1 = require("@oclif/core");
|
|
5
6
|
const cli_ux_1 = require("@oclif/core/lib/cli-ux");
|
|
6
7
|
const errors_1 = require("@oclif/core/lib/errors");
|
|
7
|
-
const archiver = require("archiver");
|
|
8
8
|
const promises_1 = require("node:fs/promises");
|
|
9
|
-
const
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
const constants_1 = require("../constants");
|
|
11
|
+
const methods_1 = require("../methods");
|
|
10
12
|
const plan_1 = require("../plan");
|
|
11
13
|
const mimeTypeLookupByExtension = {
|
|
12
14
|
apk: 'application/vnd.android.package-archive',
|
|
13
15
|
yaml: 'application/x-yaml',
|
|
14
16
|
zip: 'application/zip',
|
|
15
17
|
};
|
|
16
|
-
const PERMITTED_EXTENSIONS = new Set([
|
|
17
|
-
'yml',
|
|
18
|
-
'yaml',
|
|
19
|
-
'png',
|
|
20
|
-
'jpg',
|
|
21
|
-
'jpeg',
|
|
22
|
-
'gif',
|
|
23
|
-
'mp4',
|
|
24
|
-
'js',
|
|
25
|
-
]);
|
|
26
|
-
const typeSafePost = async (baseUrl, path, init) => {
|
|
27
|
-
const res = await fetch(baseUrl + path, { ...init, method: 'POST' });
|
|
28
|
-
if (!res.ok) {
|
|
29
|
-
throw new Error(await res.text());
|
|
30
|
-
}
|
|
31
|
-
return res.json();
|
|
32
|
-
};
|
|
33
|
-
const typeSafeGet = async (baseUrl, path, init) => {
|
|
34
|
-
const res = await fetch(baseUrl + path, init);
|
|
35
|
-
if (!res.ok) {
|
|
36
|
-
throw new Error(await res.text());
|
|
37
|
-
}
|
|
38
|
-
return res.json();
|
|
39
|
-
};
|
|
40
|
-
const toBuffer = async (archive) => {
|
|
41
|
-
const chunks = [];
|
|
42
|
-
const writable = new node_stream_1.Writable();
|
|
43
|
-
writable._write = (chunk, _, callback) => {
|
|
44
|
-
// save to array to concatenate later
|
|
45
|
-
chunks.push(chunk);
|
|
46
|
-
callback();
|
|
47
|
-
};
|
|
48
|
-
// pipe to writable
|
|
49
|
-
archive.pipe(writable);
|
|
50
|
-
await archive.finalize();
|
|
51
|
-
// once done, concatenate chunks
|
|
52
|
-
return Buffer.concat(chunks);
|
|
53
|
-
};
|
|
54
|
-
const compressDir = async (sourceDir) => {
|
|
55
|
-
// const output = createWriteStream(zipTargetPath);
|
|
56
|
-
const archive = archiver('zip', {
|
|
57
|
-
zlib: { level: 9 },
|
|
58
|
-
});
|
|
59
|
-
archive.on('error', (err) => {
|
|
60
|
-
throw err;
|
|
61
|
-
});
|
|
62
|
-
archive.directory(sourceDir, '.', (data) => {
|
|
63
|
-
if (data.name.split('/')[0] === 'node_modules')
|
|
64
|
-
return false;
|
|
65
|
-
if (PERMITTED_EXTENSIONS.has(data.name.split('.').pop())) {
|
|
66
|
-
return data;
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
69
|
-
});
|
|
70
|
-
const buffer = await toBuffer(archive);
|
|
71
|
-
// await writeFile('./my-zip.zip', buffer);
|
|
72
|
-
return buffer;
|
|
73
|
-
};
|
|
74
|
-
// const compressFile = async (sourceFile: string) => {
|
|
75
|
-
// const archive = archiver('zip', {
|
|
76
|
-
// zlib: { level: 9 },
|
|
77
|
-
// });
|
|
78
|
-
// archive.on('error', (err) => {
|
|
79
|
-
// throw err;
|
|
80
|
-
// });
|
|
81
|
-
// console.log(sourceFile, sourceFile.split('/').pop());
|
|
82
|
-
// archive.file(sourceFile.includes('/') ? sourceFile : './' + sourceFile, {
|
|
83
|
-
// name: sourceFile.split('/').pop()!,
|
|
84
|
-
// });
|
|
85
|
-
// const buffer = await toBuffer(archive);
|
|
86
|
-
// await writeFile('./my-file-zip.zip', buffer);
|
|
87
|
-
// return buffer;
|
|
88
|
-
// };
|
|
89
18
|
var EiOSDevices;
|
|
90
19
|
(function (EiOSDevices) {
|
|
91
20
|
EiOSDevices["ipad-pro-6th-gen"] = "ipad-pro-6th-gen";
|
|
@@ -103,7 +32,7 @@ var EiOSDevices;
|
|
|
103
32
|
EiOSDevices["iphone-15-plus"] = "iphone-15-plus";
|
|
104
33
|
EiOSDevices["iphone-15-pro"] = "iphone-15-pro";
|
|
105
34
|
EiOSDevices["iphone-15-pro-max"] = "iphone-15-pro-max";
|
|
106
|
-
})(EiOSDevices || (EiOSDevices = {}));
|
|
35
|
+
})(EiOSDevices || (exports.EiOSDevices = EiOSDevices = {}));
|
|
107
36
|
class Cloud extends core_1.Command {
|
|
108
37
|
static args = {
|
|
109
38
|
firstFile: core_1.Args.string({
|
|
@@ -119,109 +48,10 @@ class Cloud extends core_1.Command {
|
|
|
119
48
|
};
|
|
120
49
|
static description = `Test a Flow or set of Flows on devicecloud.dev (https://devicecloud.dev)\nProvide your application file and a folder with Maestro flows to run them in parallel on multiple devices in devicecloud.dev\nThe command will block until all analyses have completed`;
|
|
121
50
|
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
122
|
-
static flags =
|
|
123
|
-
'android-api-level': core_1.Flags.integer({
|
|
124
|
-
description: '[Android only] Android API level to run your flow against',
|
|
125
|
-
options: ['32', '33', '34'],
|
|
126
|
-
}),
|
|
127
|
-
'android-device': core_1.Flags.string({
|
|
128
|
-
description: '[Android only] Android device to run your flow against',
|
|
129
|
-
options: [
|
|
130
|
-
'pixel-6',
|
|
131
|
-
'pixel-6a',
|
|
132
|
-
'pixel-6-pro',
|
|
133
|
-
'pixel-7',
|
|
134
|
-
'pixel-7-pro',
|
|
135
|
-
'generic-tablet',
|
|
136
|
-
],
|
|
137
|
-
}),
|
|
138
|
-
apiKey: core_1.Flags.string({
|
|
139
|
-
aliases: ['api-key'],
|
|
140
|
-
description: 'API key',
|
|
141
|
-
}),
|
|
142
|
-
apiUrl: core_1.Flags.string({
|
|
143
|
-
aliases: ['api-url', 'apiURL'],
|
|
144
|
-
default: 'https://api.devicecloud.dev',
|
|
145
|
-
description: 'API base URL',
|
|
146
|
-
hidden: true,
|
|
147
|
-
}),
|
|
148
|
-
'app-binary-id': core_1.Flags.string({
|
|
149
|
-
aliases: ['app-binary-id'],
|
|
150
|
-
description: 'The ID of the app binary previously uploaded to Maestro Cloud',
|
|
151
|
-
}),
|
|
152
|
-
'app-file': core_1.Flags.file({
|
|
153
|
-
aliases: ['app-file'],
|
|
154
|
-
description: 'App binary to run your flows against',
|
|
155
|
-
}),
|
|
156
|
-
arm64: core_1.Flags.boolean({
|
|
157
|
-
default: false,
|
|
158
|
-
description: '[Android only] Run your flow against arm64 devices',
|
|
159
|
-
}),
|
|
160
|
-
async: core_1.Flags.boolean({
|
|
161
|
-
description: 'Wait for the results of the run',
|
|
162
|
-
}),
|
|
163
|
-
env: core_1.Flags.file({
|
|
164
|
-
char: 'e',
|
|
165
|
-
description: 'One or more environment variables to inject into your flows',
|
|
166
|
-
multiple: true,
|
|
167
|
-
}),
|
|
168
|
-
'exclude-tags': core_1.Flags.string({
|
|
169
|
-
aliases: ['exclude-tags'],
|
|
170
|
-
default: [],
|
|
171
|
-
description: 'Flows which have these tags will be excluded from the run',
|
|
172
|
-
multiple: true,
|
|
173
|
-
parse: (input) => input.split(','),
|
|
174
|
-
}),
|
|
175
|
-
flows: core_1.Flags.string({
|
|
176
|
-
description: 'The path to the flow file or folder containing your flows',
|
|
177
|
-
}),
|
|
178
|
-
'google-play': core_1.Flags.boolean({
|
|
179
|
-
aliases: ['google-play'],
|
|
180
|
-
default: false,
|
|
181
|
-
description: '[Android only] Run your flow against Google Play devices',
|
|
182
|
-
}),
|
|
183
|
-
'include-tags': core_1.Flags.string({
|
|
184
|
-
aliases: ['include-tags'],
|
|
185
|
-
default: [],
|
|
186
|
-
description: 'Only flows which have these tags will be included in the run',
|
|
187
|
-
multiple: true,
|
|
188
|
-
parse: (input) => input.split(','),
|
|
189
|
-
}),
|
|
190
|
-
'ios-device': core_1.Flags.string({
|
|
191
|
-
description: '[iOS only] iOS device to run your flow against',
|
|
192
|
-
options: [
|
|
193
|
-
'iphone-12',
|
|
194
|
-
'iphone-12-mini',
|
|
195
|
-
'iphone-12-pro-max',
|
|
196
|
-
'iphone-13',
|
|
197
|
-
'iphone-13-mini',
|
|
198
|
-
'iphone-13-pro-max',
|
|
199
|
-
'iphone-14',
|
|
200
|
-
'iphone-14-plus',
|
|
201
|
-
'iphone-14-pro',
|
|
202
|
-
'iphone-14-pro-max',
|
|
203
|
-
'iphone-15',
|
|
204
|
-
'iphone-15-plus',
|
|
205
|
-
'iphone-15-pro',
|
|
206
|
-
'iphone-15-pro-max',
|
|
207
|
-
'ipad-pro-6th-gen',
|
|
208
|
-
],
|
|
209
|
-
}),
|
|
210
|
-
'ios-version': core_1.Flags.string({
|
|
211
|
-
description: '[iOS only] iOS version to run your flow against',
|
|
212
|
-
options: ['15', '16', '17'],
|
|
213
|
-
}),
|
|
214
|
-
name: core_1.Flags.string({
|
|
215
|
-
description: 'A custom name for your upload (useful for tagging commits etc)',
|
|
216
|
-
}),
|
|
217
|
-
orientation: core_1.Flags.string({
|
|
218
|
-
description: '[Android only] The orientation of the device to run your flow against in degrees',
|
|
219
|
-
options: ['0', '90', '180', '270'],
|
|
220
|
-
}),
|
|
221
|
-
};
|
|
51
|
+
static flags = constants_1.flags;
|
|
222
52
|
async run() {
|
|
223
53
|
try {
|
|
224
|
-
const { args, flags } = await this.parse(Cloud);
|
|
54
|
+
const { args, flags, raw } = await this.parse(Cloud);
|
|
225
55
|
const { 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, arm64, async, env, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, name, orientation, ...rest } = flags;
|
|
226
56
|
if (arm64) {
|
|
227
57
|
(0, cli_ux_1.info)('Contact hello@devicecloud.dev to enquire about arm64 devices');
|
|
@@ -241,46 +71,29 @@ class Cloud extends core_1.Command {
|
|
|
241
71
|
throw new Error('You must provide a flow file');
|
|
242
72
|
}
|
|
243
73
|
if (iOSVersion) {
|
|
244
|
-
const iOSCompatibilityLookup = {
|
|
245
|
-
'ipad-pro-6th-gen': ['16', '17'],
|
|
246
|
-
'iphone-12': ['15', '16', '17'],
|
|
247
|
-
'iphone-12-mini': ['15', '16', '17'],
|
|
248
|
-
'iphone-12-pro-max': ['15', '16', '17'],
|
|
249
|
-
'iphone-13': ['15', '16', '17'],
|
|
250
|
-
'iphone-13-mini': ['15', '16', '17'],
|
|
251
|
-
'iphone-13-pro-max': ['15', '16', '17'],
|
|
252
|
-
'iphone-14': ['16', '17'],
|
|
253
|
-
'iphone-14-plus': ['16', '17'],
|
|
254
|
-
'iphone-14-pro': ['16', '17'],
|
|
255
|
-
'iphone-14-pro-max': ['16', '17'],
|
|
256
|
-
'iphone-15': ['17'],
|
|
257
|
-
'iphone-15-plus': ['17'],
|
|
258
|
-
'iphone-15-pro': ['17'],
|
|
259
|
-
'iphone-15-pro-max': ['17'],
|
|
260
|
-
};
|
|
261
74
|
const iOSDeviceID = iOSDevice || 'iphone-14';
|
|
262
|
-
const supportediOSVersions = iOSCompatibilityLookup[iOSDeviceID];
|
|
75
|
+
const supportediOSVersions = constants_1.iOSCompatibilityLookup[iOSDeviceID];
|
|
263
76
|
if (!supportediOSVersions.includes(iOSVersion)) {
|
|
264
77
|
throw new Error(`${iOSDeviceID} only supports these iOS versions: ${supportediOSVersions.join(',')}`);
|
|
265
78
|
}
|
|
266
79
|
}
|
|
267
|
-
|
|
268
|
-
let continueOnFailure = true;
|
|
269
|
-
let sequentialFlows = [];
|
|
80
|
+
flowFile = path.resolve(flowFile);
|
|
270
81
|
if (!flowFile?.endsWith('.yaml') &&
|
|
271
82
|
!flowFile?.endsWith('.yml') &&
|
|
272
83
|
!flowFile?.endsWith('/')) {
|
|
273
84
|
flowFile += '/';
|
|
274
85
|
}
|
|
86
|
+
let executionPlan;
|
|
275
87
|
try {
|
|
276
|
-
|
|
277
|
-
testFileNames = executionPlan.flowsToRun;
|
|
278
|
-
continueOnFailure = executionPlan.sequence?.continueOnFailure ?? true;
|
|
279
|
-
sequentialFlows = executionPlan.sequence?.flows ?? [];
|
|
88
|
+
executionPlan = await (0, plan_1.plan)(flowFile, includeTags.flat(), excludeTags.flat());
|
|
280
89
|
}
|
|
281
90
|
catch (error) {
|
|
282
91
|
console.error(error);
|
|
92
|
+
// eslint-disable-next-line no-process-exit, unicorn/no-process-exit
|
|
93
|
+
process.exit(2);
|
|
283
94
|
}
|
|
95
|
+
const { allExcludeTags, allIncludeTags, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
96
|
+
const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
|
|
284
97
|
if (!appBinaryId) {
|
|
285
98
|
if (!(flowFile && finalAppFile)) {
|
|
286
99
|
throw new Error('You must provide a flow file and an app binary id');
|
|
@@ -288,6 +101,9 @@ class Cloud extends core_1.Command {
|
|
|
288
101
|
if (!finalAppFile.endsWith('.apk') && !finalAppFile.endsWith('.zip')) {
|
|
289
102
|
throw new Error('App file must be a .apk or .zip file');
|
|
290
103
|
}
|
|
104
|
+
if (finalAppFile.endsWith('.zip')) {
|
|
105
|
+
await (0, methods_1.verifyAppZip)(finalAppFile);
|
|
106
|
+
}
|
|
291
107
|
}
|
|
292
108
|
const flagLogs = [];
|
|
293
109
|
for (const [k, v] of Object.entries(flags)) {
|
|
@@ -297,15 +113,15 @@ class Cloud extends core_1.Command {
|
|
|
297
113
|
}
|
|
298
114
|
this.log(`
|
|
299
115
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
116
|
+
Submitting new job
|
|
117
|
+
→ Flow(s): ${flowFile}
|
|
118
|
+
→ App: ${appBinaryId || finalAppFile}
|
|
303
119
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
120
|
+
With options
|
|
121
|
+
→ ${flagLogs.join(`
|
|
122
|
+
→ `)}
|
|
307
123
|
|
|
308
|
-
|
|
124
|
+
`);
|
|
309
125
|
if (!finalBinaryId) {
|
|
310
126
|
core_1.ux.action.start('Uploading binary', 'Initializing', { stdout: true });
|
|
311
127
|
const binaryFormData = new FormData();
|
|
@@ -318,7 +134,7 @@ class Cloud extends core_1.Command {
|
|
|
318
134
|
headers: { 'x-app-api-key': apiKey },
|
|
319
135
|
};
|
|
320
136
|
core_1.ux.action.status = `Uploading`;
|
|
321
|
-
const { binaryId, message } = await typeSafePost(apiUrl, '/uploads/binary', options);
|
|
137
|
+
const { binaryId, message } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/binary', options);
|
|
322
138
|
if (!binaryId)
|
|
323
139
|
throw new Error(message);
|
|
324
140
|
core_1.ux.action.stop(`\nBinary uploaded with id: ${binaryId}`);
|
|
@@ -331,14 +147,15 @@ class Cloud extends core_1.Command {
|
|
|
331
147
|
acc[key] = value;
|
|
332
148
|
return acc;
|
|
333
149
|
}, {});
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
150
|
+
const buffer = await (0, methods_1.compressFilesFromRelativePath)(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
|
|
151
|
+
? path.dirname(flowFile)
|
|
152
|
+
: flowFile, [
|
|
153
|
+
...new Set([
|
|
154
|
+
...referencedFiles,
|
|
155
|
+
...testFileNames,
|
|
156
|
+
...sequentialFlows,
|
|
157
|
+
]),
|
|
158
|
+
]);
|
|
342
159
|
const blob = new Blob([buffer], {
|
|
343
160
|
type: mimeTypeLookupByExtension.zip,
|
|
344
161
|
});
|
|
@@ -348,22 +165,23 @@ class Cloud extends core_1.Command {
|
|
|
348
165
|
testFormData.set('sequentialFlows', JSON.stringify(sequentialFlows));
|
|
349
166
|
testFormData.set('env', JSON.stringify(envObject));
|
|
350
167
|
testFormData.set('googlePlay', googlePlay ? 'true' : 'false');
|
|
351
|
-
testFormData.set('config', JSON.stringify({
|
|
352
|
-
|
|
168
|
+
testFormData.set('config', JSON.stringify({
|
|
169
|
+
allExcludeTags,
|
|
170
|
+
allIncludeTags,
|
|
171
|
+
continueOnFailure,
|
|
172
|
+
orientation,
|
|
173
|
+
raw,
|
|
174
|
+
}));
|
|
175
|
+
if (androidApiLevel)
|
|
353
176
|
testFormData.set('androidApiLevel', androidApiLevel.toString());
|
|
354
|
-
|
|
355
|
-
if (androidDevice) {
|
|
177
|
+
if (androidDevice)
|
|
356
178
|
testFormData.set('androidDevice', androidDevice.toString());
|
|
357
|
-
|
|
358
|
-
if (iOSVersion) {
|
|
179
|
+
if (iOSVersion)
|
|
359
180
|
testFormData.set('iOSVersion', iOSVersion.toString());
|
|
360
|
-
|
|
361
|
-
if (iOSDevice) {
|
|
181
|
+
if (iOSDevice)
|
|
362
182
|
testFormData.set('iOSDevice', iOSDevice.toString());
|
|
363
|
-
|
|
364
|
-
if (name) {
|
|
183
|
+
if (name)
|
|
365
184
|
testFormData.set('name', name.toString());
|
|
366
|
-
}
|
|
367
185
|
for (const [key, value] of Object.entries(rest)) {
|
|
368
186
|
if (value) {
|
|
369
187
|
testFormData.set(key, value);
|
|
@@ -373,7 +191,7 @@ class Cloud extends core_1.Command {
|
|
|
373
191
|
body: testFormData,
|
|
374
192
|
headers: { 'x-app-api-key': apiKey },
|
|
375
193
|
};
|
|
376
|
-
const { message, results } = await typeSafePost(apiUrl, '/uploads/flow', options);
|
|
194
|
+
const { message, results } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/flow', options);
|
|
377
195
|
if (!results?.length)
|
|
378
196
|
(0, errors_1.error)('No tests created: ' + message);
|
|
379
197
|
(0, cli_ux_1.info)(`\nCreated ${results.length} tests: ${results
|
|
@@ -390,7 +208,7 @@ class Cloud extends core_1.Command {
|
|
|
390
208
|
core_1.ux.action.start('Waiting for results', 'Initializing', { stdout: true });
|
|
391
209
|
(0, cli_ux_1.info)('\nYou can safely close this terminal and the tests will continue\n');
|
|
392
210
|
const intervalId = setInterval(async () => {
|
|
393
|
-
const { results: updatedResults } = await typeSafeGet(apiUrl, `/results/${results[0].test_upload_id}`, {
|
|
211
|
+
const { results: updatedResults } = await (0, methods_1.typeSafeGet)(apiUrl, `/results/${results[0].test_upload_id}`, {
|
|
394
212
|
headers: { 'x-app-api-key': apiKey },
|
|
395
213
|
});
|
|
396
214
|
if (!updatedResults) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { EiOSDevices } from './commands/cloud';
|
|
2
|
+
export declare const flags: {
|
|
3
|
+
'android-api-level': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
4
|
+
'android-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
5
|
+
apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
6
|
+
apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
7
|
+
'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
|
+
arm64: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
env: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
|
+
'exclude-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
+
flows: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
|
+
'google-play': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
16
|
+
'ios-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
17
|
+
'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
18
|
+
name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
19
|
+
orientation: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
20
|
+
};
|
|
21
|
+
export declare const iOSCompatibilityLookup: {
|
|
22
|
+
[k in EiOSDevices]: string[];
|
|
23
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.iOSCompatibilityLookup = exports.flags = void 0;
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
exports.flags = {
|
|
6
|
+
'android-api-level': core_1.Flags.integer({
|
|
7
|
+
description: '[Android only] Android API level to run your flow against',
|
|
8
|
+
options: ['32', '33', '34'],
|
|
9
|
+
}),
|
|
10
|
+
'android-device': core_1.Flags.string({
|
|
11
|
+
description: '[Android only] Android device to run your flow against',
|
|
12
|
+
options: [
|
|
13
|
+
'pixel-6',
|
|
14
|
+
'pixel-6a',
|
|
15
|
+
'pixel-6-pro',
|
|
16
|
+
'pixel-7',
|
|
17
|
+
'pixel-7-pro',
|
|
18
|
+
'generic-tablet',
|
|
19
|
+
],
|
|
20
|
+
}),
|
|
21
|
+
apiKey: core_1.Flags.string({
|
|
22
|
+
aliases: ['api-key'],
|
|
23
|
+
description: 'API key',
|
|
24
|
+
}),
|
|
25
|
+
apiUrl: core_1.Flags.string({
|
|
26
|
+
aliases: ['api-url', 'apiURL'],
|
|
27
|
+
default: 'https://api.devicecloud.dev',
|
|
28
|
+
description: 'API base URL',
|
|
29
|
+
hidden: true,
|
|
30
|
+
}),
|
|
31
|
+
'app-binary-id': core_1.Flags.string({
|
|
32
|
+
aliases: ['app-binary-id'],
|
|
33
|
+
description: 'The ID of the app binary previously uploaded to Maestro Cloud',
|
|
34
|
+
}),
|
|
35
|
+
'app-file': core_1.Flags.file({
|
|
36
|
+
aliases: ['app-file'],
|
|
37
|
+
description: 'App binary to run your flows against',
|
|
38
|
+
}),
|
|
39
|
+
arm64: core_1.Flags.boolean({
|
|
40
|
+
default: false,
|
|
41
|
+
description: '[Android only] Run your flow against arm64 devices',
|
|
42
|
+
}),
|
|
43
|
+
async: core_1.Flags.boolean({
|
|
44
|
+
description: 'Wait for the results of the run',
|
|
45
|
+
}),
|
|
46
|
+
env: core_1.Flags.file({
|
|
47
|
+
char: 'e',
|
|
48
|
+
description: 'One or more environment variables to inject into your flows',
|
|
49
|
+
multiple: true,
|
|
50
|
+
}),
|
|
51
|
+
'exclude-tags': core_1.Flags.string({
|
|
52
|
+
aliases: ['exclude-tags'],
|
|
53
|
+
default: [],
|
|
54
|
+
description: 'Flows which have these tags will be excluded from the run',
|
|
55
|
+
multiple: true,
|
|
56
|
+
parse: (input) => input.split(','),
|
|
57
|
+
}),
|
|
58
|
+
flows: core_1.Flags.string({
|
|
59
|
+
description: 'The path to the flow file or folder containing your flows',
|
|
60
|
+
}),
|
|
61
|
+
'google-play': core_1.Flags.boolean({
|
|
62
|
+
aliases: ['google-play'],
|
|
63
|
+
default: false,
|
|
64
|
+
description: '[Android only] Run your flow against Google Play devices',
|
|
65
|
+
}),
|
|
66
|
+
'include-tags': core_1.Flags.string({
|
|
67
|
+
aliases: ['include-tags'],
|
|
68
|
+
default: [],
|
|
69
|
+
description: 'Only flows which have these tags will be included in the run',
|
|
70
|
+
multiple: true,
|
|
71
|
+
parse: (input) => input.split(','),
|
|
72
|
+
}),
|
|
73
|
+
'ios-device': core_1.Flags.string({
|
|
74
|
+
description: '[iOS only] iOS device to run your flow against',
|
|
75
|
+
options: [
|
|
76
|
+
'iphone-12',
|
|
77
|
+
'iphone-12-mini',
|
|
78
|
+
'iphone-12-pro-max',
|
|
79
|
+
'iphone-13',
|
|
80
|
+
'iphone-13-mini',
|
|
81
|
+
'iphone-13-pro-max',
|
|
82
|
+
'iphone-14',
|
|
83
|
+
'iphone-14-plus',
|
|
84
|
+
'iphone-14-pro',
|
|
85
|
+
'iphone-14-pro-max',
|
|
86
|
+
'iphone-15',
|
|
87
|
+
'iphone-15-plus',
|
|
88
|
+
'iphone-15-pro',
|
|
89
|
+
'iphone-15-pro-max',
|
|
90
|
+
'ipad-pro-6th-gen',
|
|
91
|
+
],
|
|
92
|
+
}),
|
|
93
|
+
'ios-version': core_1.Flags.string({
|
|
94
|
+
description: '[iOS only] iOS version to run your flow against',
|
|
95
|
+
options: ['15', '16', '17'],
|
|
96
|
+
}),
|
|
97
|
+
name: core_1.Flags.string({
|
|
98
|
+
description: 'A custom name for your upload (useful for tagging commits etc)',
|
|
99
|
+
}),
|
|
100
|
+
orientation: core_1.Flags.string({
|
|
101
|
+
description: '[Android only] The orientation of the device to run your flow against in degrees',
|
|
102
|
+
options: ['0', '90', '180', '270'],
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
exports.iOSCompatibilityLookup = {
|
|
106
|
+
'ipad-pro-6th-gen': ['16', '17'],
|
|
107
|
+
'iphone-12': ['15', '16', '17'],
|
|
108
|
+
'iphone-12-mini': ['15', '16', '17'],
|
|
109
|
+
'iphone-12-pro-max': ['15', '16', '17'],
|
|
110
|
+
'iphone-13': ['15', '16', '17'],
|
|
111
|
+
'iphone-13-mini': ['15', '16', '17'],
|
|
112
|
+
'iphone-13-pro-max': ['15', '16', '17'],
|
|
113
|
+
'iphone-14': ['16', '17'],
|
|
114
|
+
'iphone-14-plus': ['16', '17'],
|
|
115
|
+
'iphone-14-pro': ['16', '17'],
|
|
116
|
+
'iphone-14-pro-max': ['16', '17'],
|
|
117
|
+
'iphone-15': ['17'],
|
|
118
|
+
'iphone-15-plus': ['17'],
|
|
119
|
+
'iphone-15-pro': ['17'],
|
|
120
|
+
'iphone-15-pro-max': ['17'],
|
|
121
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import * as archiver from 'archiver';
|
|
3
|
+
import { paths } from '../../api/schema.types';
|
|
4
|
+
export declare const typeSafePost: <T extends keyof paths>(baseUrl: string, path: string, init?: {
|
|
5
|
+
body?: FormData;
|
|
6
|
+
headers?: HeadersInit;
|
|
7
|
+
}) => Promise<paths[T]["post"]["responses"]["201"]["content"]["application/json"]>;
|
|
8
|
+
export declare const typeSafeGet: <T extends keyof paths>(baseUrl: string, path: string, init?: {
|
|
9
|
+
body?: FormData;
|
|
10
|
+
headers?: HeadersInit;
|
|
11
|
+
}) => Promise<paths[T]["get"]["responses"]["200"]["content"]["application/json"]>;
|
|
12
|
+
export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer>;
|
|
13
|
+
export declare const compressDir: (sourceDir: string) => Promise<Buffer>;
|
|
14
|
+
export declare const compressFilesFromRelativePath: (path: string, files: string[]) => Promise<Buffer>;
|
|
15
|
+
export declare const verifyAppZip: (zipPath: string) => Promise<void>;
|
package/dist/methods.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressDir = exports.toBuffer = exports.typeSafeGet = exports.typeSafePost = void 0;
|
|
4
|
+
const archiver = require("archiver");
|
|
5
|
+
// import { writeFile } from 'node:fs/promises';
|
|
6
|
+
const nodePath = require("node:path");
|
|
7
|
+
const node_stream_1 = require("node:stream");
|
|
8
|
+
const StreamZip = require("node-stream-zip");
|
|
9
|
+
const PERMITTED_EXTENSIONS = new Set([
|
|
10
|
+
'yml',
|
|
11
|
+
'yaml',
|
|
12
|
+
'png',
|
|
13
|
+
'jpg',
|
|
14
|
+
'jpeg',
|
|
15
|
+
'gif',
|
|
16
|
+
'mp4',
|
|
17
|
+
'js',
|
|
18
|
+
]);
|
|
19
|
+
const typeSafePost = async (baseUrl, path, init) => {
|
|
20
|
+
const res = await fetch(baseUrl + path, { ...init, method: 'POST' });
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
throw new Error(await res.text());
|
|
23
|
+
}
|
|
24
|
+
return res.json();
|
|
25
|
+
};
|
|
26
|
+
exports.typeSafePost = typeSafePost;
|
|
27
|
+
const typeSafeGet = async (baseUrl, path, init) => {
|
|
28
|
+
const res = await fetch(baseUrl + path, init);
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
throw new Error(await res.text());
|
|
31
|
+
}
|
|
32
|
+
return res.json();
|
|
33
|
+
};
|
|
34
|
+
exports.typeSafeGet = typeSafeGet;
|
|
35
|
+
const toBuffer = async (archive) => {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
const writable = new node_stream_1.Writable();
|
|
38
|
+
writable._write = (chunk, _, callback) => {
|
|
39
|
+
// save to array to concatenate later
|
|
40
|
+
chunks.push(chunk);
|
|
41
|
+
callback();
|
|
42
|
+
};
|
|
43
|
+
// pipe to writable
|
|
44
|
+
archive.pipe(writable);
|
|
45
|
+
await archive.finalize();
|
|
46
|
+
// once done, concatenate chunks
|
|
47
|
+
return Buffer.concat(chunks);
|
|
48
|
+
};
|
|
49
|
+
exports.toBuffer = toBuffer;
|
|
50
|
+
const compressDir = async (sourceDir) => {
|
|
51
|
+
// const output = createWriteStream(zipTargetPath);
|
|
52
|
+
const archive = archiver('zip', {
|
|
53
|
+
zlib: { level: 9 },
|
|
54
|
+
});
|
|
55
|
+
archive.on('error', (err) => {
|
|
56
|
+
throw err;
|
|
57
|
+
});
|
|
58
|
+
archive.directory(sourceDir, '.', (data) => {
|
|
59
|
+
if (data.name.split('/')[0] === 'node_modules')
|
|
60
|
+
return false;
|
|
61
|
+
if (PERMITTED_EXTENSIONS.has(data.name.split('.').pop())) {
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
});
|
|
66
|
+
const buffer = await (0, exports.toBuffer)(archive);
|
|
67
|
+
// await writeFile('./my-zip.zip', buffer);
|
|
68
|
+
return buffer;
|
|
69
|
+
};
|
|
70
|
+
exports.compressDir = compressDir;
|
|
71
|
+
const compressFilesFromRelativePath = async (path, files) => {
|
|
72
|
+
const archive = archiver('zip', {
|
|
73
|
+
zlib: { level: 9 },
|
|
74
|
+
});
|
|
75
|
+
archive.on('error', (err) => {
|
|
76
|
+
throw err;
|
|
77
|
+
});
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
archive.file(nodePath.resolve(path, file), { name: file });
|
|
80
|
+
}
|
|
81
|
+
const buffer = await (0, exports.toBuffer)(archive);
|
|
82
|
+
// await writeFile('./my-zip.zip', buffer);
|
|
83
|
+
return buffer;
|
|
84
|
+
};
|
|
85
|
+
exports.compressFilesFromRelativePath = compressFilesFromRelativePath;
|
|
86
|
+
const verifyAppZip = async (zipPath) => {
|
|
87
|
+
// eslint-disable-next-line import/namespace, new-cap
|
|
88
|
+
const zip = await new StreamZip.async({
|
|
89
|
+
file: zipPath,
|
|
90
|
+
storeEntries: true,
|
|
91
|
+
});
|
|
92
|
+
const entries = await zip.entries();
|
|
93
|
+
const topLevelEntries = Object.values(entries).filter((entry) => !entry.name.split('/')[1]);
|
|
94
|
+
if (topLevelEntries.length !== 1 ||
|
|
95
|
+
!topLevelEntries[0].name.endsWith('.app/')) {
|
|
96
|
+
throw new Error('Zip file must contain exactly one entry which is a .app, check the contents of the zip file');
|
|
97
|
+
}
|
|
98
|
+
zip.close();
|
|
99
|
+
};
|
|
100
|
+
exports.verifyAppZip = verifyAppZip;
|
package/dist/plan.d.ts
CHANGED
package/dist/plan.js
CHANGED
|
@@ -1,76 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.plan = void 0;
|
|
4
|
-
|
|
5
|
-
const
|
|
4
|
+
/* eslint-disable complexity */
|
|
5
|
+
const glob_1 = require("glob");
|
|
6
6
|
const fs = require("node:fs");
|
|
7
7
|
const path = require("node:path");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return [];
|
|
11
|
-
const orderSet = new Set(flowOrder);
|
|
12
|
-
const namesInOrder = Object.keys(paths).filter((key) => orderSet.has(key));
|
|
13
|
-
if (namesInOrder.length === 0)
|
|
14
|
-
return [];
|
|
15
|
-
const result = [...orderSet].filter((item) => namesInOrder.includes(item));
|
|
16
|
-
if (result.length === 0) {
|
|
17
|
-
throw new Error(`Could not find flows needed for execution in order: ${[...orderSet]
|
|
18
|
-
.filter((item) => !namesInOrder.includes(item))
|
|
19
|
-
.join(', ')}`);
|
|
20
|
-
}
|
|
21
|
-
else if (flowOrder
|
|
22
|
-
.slice(0, result.length)
|
|
23
|
-
.every((value, index) => value === result[index])) {
|
|
24
|
-
return result.map((item) => paths[item]);
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function isFlowFile(filePath) {
|
|
31
|
-
return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
|
|
32
|
-
}
|
|
33
|
-
const readYamlFileAsJson = (filePath) => {
|
|
34
|
-
const yamlText = fs.readFileSync(filePath, 'utf8');
|
|
35
|
-
return yaml.load(yamlText);
|
|
36
|
-
};
|
|
37
|
-
const readConfigFromYamlFileAsJson = (filePath) => {
|
|
38
|
-
const yamlText = fs.readFileSync(filePath, 'utf8');
|
|
39
|
-
if (yamlText.includes('\n---\n')) {
|
|
40
|
-
const yamlTexts = yamlText.split('\n---\n');
|
|
41
|
-
const config = yaml.load(yamlTexts[0]);
|
|
42
|
-
if (Object.keys(config).length > 0) {
|
|
43
|
-
return config;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
};
|
|
48
|
-
async function walk(dir, filterFunction) {
|
|
49
|
-
const readDirResult = await fs.promises.readdir(dir);
|
|
50
|
-
const files = await Promise.all(readDirResult.map(async (file) => {
|
|
51
|
-
const filePath = path.join(dir, file);
|
|
52
|
-
const stats = await fs.promises.stat(filePath);
|
|
53
|
-
if (stats.isDirectory())
|
|
54
|
-
return walk(filePath);
|
|
55
|
-
if (stats.isFile())
|
|
56
|
-
if (filterFunction) {
|
|
57
|
-
if (filterFunction(filePath))
|
|
58
|
-
return filePath;
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
return filePath;
|
|
62
|
-
}
|
|
63
|
-
}));
|
|
64
|
-
return files.flat().filter(Boolean);
|
|
65
|
-
}
|
|
8
|
+
const planMethods_1 = require("./planMethods");
|
|
9
|
+
const commandsThatRequireFiles = new Set(['addMedia', 'runFlow', 'runScript']);
|
|
66
10
|
async function plan(input, includeTags, excludeTags) {
|
|
67
11
|
if (!fs.existsSync(input)) {
|
|
68
12
|
throw new Error(`Flow path does not exist: ${path.resolve(input)}`);
|
|
69
13
|
}
|
|
70
14
|
if (fs.lstatSync(input).isFile()) {
|
|
71
|
-
|
|
15
|
+
const directory = path.dirname(input);
|
|
16
|
+
const { testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(input);
|
|
17
|
+
let errors = [];
|
|
18
|
+
let allFiles = [];
|
|
19
|
+
for (const command of testSteps) {
|
|
20
|
+
if (typeof command === 'string')
|
|
21
|
+
continue;
|
|
22
|
+
for (const [commandName, commandValue] of Object.entries(command)) {
|
|
23
|
+
if (commandsThatRequireFiles.has(commandName)) {
|
|
24
|
+
const { errors: newErrors, files } = (0, planMethods_1.checkIfFilesExistInWorkspace)(commandName, commandValue, path.normalize(input), directory + '/');
|
|
25
|
+
errors = [...errors, ...newErrors];
|
|
26
|
+
allFiles = [...allFiles, ...files];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (errors.length > 0) {
|
|
31
|
+
throw new Error('The following flow files are not present in the provided directory: \n' +
|
|
32
|
+
errors.join('\n'));
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
flowsToRun: [input.split('/').pop() ?? input],
|
|
36
|
+
referencedFiles: [...new Set(allFiles)],
|
|
37
|
+
totalFlowFiles: 1,
|
|
38
|
+
};
|
|
72
39
|
}
|
|
73
|
-
let unfilteredFlowFiles = await walk(input, isFlowFile);
|
|
40
|
+
let unfilteredFlowFiles = await (0, planMethods_1.walk)(input, planMethods_1.isFlowFile);
|
|
74
41
|
if (unfilteredFlowFiles.length === 0) {
|
|
75
42
|
throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(input)}`);
|
|
76
43
|
}
|
|
@@ -81,12 +48,17 @@ async function plan(input, includeTags, excludeTags) {
|
|
|
81
48
|
const relativeFilePaths = unfilteredFlowFiles.map((file) => file.replace(cleanPath, ''));
|
|
82
49
|
const topLevelFlowFiles = relativeFilePaths.filter((file) => !file.includes('/'));
|
|
83
50
|
const workspaceConfig = configFilePath
|
|
84
|
-
? readYamlFileAsJson(configFilePath)
|
|
51
|
+
? (0, planMethods_1.readYamlFileAsJson)(configFilePath)
|
|
85
52
|
: {};
|
|
86
53
|
let unsortedFlowFiles = topLevelFlowFiles;
|
|
87
54
|
if (workspaceConfig.flows) {
|
|
88
|
-
const
|
|
89
|
-
|
|
55
|
+
const globs = workspaceConfig.flows.map((glob) => glob);
|
|
56
|
+
const matchedFiles = await (0, glob_1.glob)(globs, {
|
|
57
|
+
cwd: input,
|
|
58
|
+
nodir: true,
|
|
59
|
+
});
|
|
60
|
+
unsortedFlowFiles = matchedFiles.filter((file) => file !== 'config.yaml' &&
|
|
61
|
+
(file.endsWith('.yaml') || file.endsWith('.yml')));
|
|
90
62
|
}
|
|
91
63
|
if (unsortedFlowFiles.length === 0) {
|
|
92
64
|
const error = workspaceConfig.flows
|
|
@@ -95,10 +67,42 @@ async function plan(input, includeTags, excludeTags) {
|
|
|
95
67
|
throw error;
|
|
96
68
|
}
|
|
97
69
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
98
|
-
const configPerFlowFile = unsortedFlowFiles.reduce((acc, filePath) => {
|
|
99
|
-
|
|
70
|
+
const { configPerFlowFile } = unsortedFlowFiles.reduce((acc, filePath) => {
|
|
71
|
+
const { config } = (0, planMethods_1.readTestYamlFileAsJson)(cleanPath + filePath);
|
|
72
|
+
acc.configPerFlowFile[filePath] = config;
|
|
100
73
|
return acc;
|
|
101
|
-
}, {
|
|
74
|
+
}, {
|
|
75
|
+
configPerFlowFile: {},
|
|
76
|
+
});
|
|
77
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
78
|
+
const { testStepsPerFlowFile } = unfilteredFlowFiles.reduce((acc, filePath) => {
|
|
79
|
+
const { testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(filePath);
|
|
80
|
+
acc.testStepsPerFlowFile[filePath] = testSteps;
|
|
81
|
+
return acc;
|
|
82
|
+
}, {
|
|
83
|
+
testStepsPerFlowFile: {},
|
|
84
|
+
});
|
|
85
|
+
let errors = [];
|
|
86
|
+
let allFiles = [];
|
|
87
|
+
for (const [filePath, commands] of Object.entries(testStepsPerFlowFile)) {
|
|
88
|
+
if (!commands)
|
|
89
|
+
break;
|
|
90
|
+
for (const command of commands) {
|
|
91
|
+
if (typeof command === 'string')
|
|
92
|
+
continue;
|
|
93
|
+
for (const [commandName, commandValue] of Object.entries(command)) {
|
|
94
|
+
if (commandsThatRequireFiles.has(commandName)) {
|
|
95
|
+
const { errors: newErrors, files } = (0, planMethods_1.checkIfFilesExistInWorkspace)(commandName, commandValue, filePath, cleanPath);
|
|
96
|
+
errors = [...errors, ...newErrors];
|
|
97
|
+
allFiles = [...allFiles, ...files];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (errors.length > 0) {
|
|
103
|
+
throw new Error('The following flow files are not present in the provided directory: \n' +
|
|
104
|
+
errors.join('\n'));
|
|
105
|
+
}
|
|
102
106
|
const allIncludeTags = [
|
|
103
107
|
...includeTags,
|
|
104
108
|
...(workspaceConfig.includeTags || []),
|
|
@@ -125,7 +129,7 @@ async function plan(input, includeTags, excludeTags) {
|
|
|
125
129
|
return acc;
|
|
126
130
|
}, {});
|
|
127
131
|
const flowsToRunInSequence = workspaceConfig.executionOrder?.flowsOrder
|
|
128
|
-
?.map((flowOrder) => getFlowsToRunInSequence(pathsByName, [flowOrder]))
|
|
132
|
+
?.map((flowOrder) => (0, planMethods_1.getFlowsToRunInSequence)(pathsByName, [flowOrder]))
|
|
129
133
|
.flat() || [];
|
|
130
134
|
const normalFlows = allFlows.filter((flow) => !flowsToRunInSequence.includes(flow));
|
|
131
135
|
// for (const filePath of allFlows) {
|
|
@@ -142,7 +146,10 @@ async function plan(input, includeTags, excludeTags) {
|
|
|
142
146
|
// );
|
|
143
147
|
// }
|
|
144
148
|
return {
|
|
149
|
+
allExcludeTags,
|
|
150
|
+
allIncludeTags,
|
|
145
151
|
flowsToRun: normalFlows,
|
|
152
|
+
referencedFiles: [...new Set(allFiles)],
|
|
146
153
|
sequence: {
|
|
147
154
|
continueOnFailure: workspaceConfig.executionOrder?.continueOnFailure,
|
|
148
155
|
flows: flowsToRunInSequence,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function getFlowsToRunInSequence(paths: {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
}, flowOrder: string[]): string[];
|
|
4
|
+
export declare function isFlowFile(filePath: string): boolean;
|
|
5
|
+
export declare const readYamlFileAsJson: (filePath: string) => unknown;
|
|
6
|
+
export declare const readTestYamlFileAsJson: (filePath: string) => {
|
|
7
|
+
config: Record<string, unknown>;
|
|
8
|
+
testSteps: Record<string, unknown>[];
|
|
9
|
+
} | {
|
|
10
|
+
config: null;
|
|
11
|
+
testSteps: Record<string, unknown>[];
|
|
12
|
+
};
|
|
13
|
+
export declare function walk(dir: string, filterFunction?: (filePath: string) => boolean): Promise<string[]>;
|
|
14
|
+
export declare const checkIfFilesExistInWorkspace: (commandName: string, command: Record<string, string> | string | string[], filePath: string, cleanPath: string) => {
|
|
15
|
+
errors: string[];
|
|
16
|
+
files: string[];
|
|
17
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkIfFilesExistInWorkspace = exports.walk = exports.readTestYamlFileAsJson = exports.readYamlFileAsJson = exports.isFlowFile = exports.getFlowsToRunInSequence = void 0;
|
|
4
|
+
/* eslint-disable unicorn/filename-case */
|
|
5
|
+
const yaml = require("js-yaml");
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
function getFlowsToRunInSequence(paths, flowOrder) {
|
|
9
|
+
if (flowOrder.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const orderSet = new Set(flowOrder);
|
|
12
|
+
const namesInOrder = Object.keys(paths).filter((key) => orderSet.has(key));
|
|
13
|
+
if (namesInOrder.length === 0)
|
|
14
|
+
return [];
|
|
15
|
+
const result = [...orderSet].filter((item) => namesInOrder.includes(item));
|
|
16
|
+
if (result.length === 0) {
|
|
17
|
+
throw new Error(`Could not find flows needed for execution in order: ${[...orderSet]
|
|
18
|
+
.filter((item) => !namesInOrder.includes(item))
|
|
19
|
+
.join(', ')}`);
|
|
20
|
+
}
|
|
21
|
+
else if (flowOrder
|
|
22
|
+
.slice(0, result.length)
|
|
23
|
+
.every((value, index) => value === result[index])) {
|
|
24
|
+
return result.map((item) => paths[item]);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.getFlowsToRunInSequence = getFlowsToRunInSequence;
|
|
31
|
+
function isFlowFile(filePath) {
|
|
32
|
+
return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
|
|
33
|
+
}
|
|
34
|
+
exports.isFlowFile = isFlowFile;
|
|
35
|
+
const readYamlFileAsJson = (filePath) => {
|
|
36
|
+
const yamlText = fs.readFileSync(filePath, 'utf8');
|
|
37
|
+
return yaml.load(yamlText);
|
|
38
|
+
};
|
|
39
|
+
exports.readYamlFileAsJson = readYamlFileAsJson;
|
|
40
|
+
const readTestYamlFileAsJson = (filePath) => {
|
|
41
|
+
const yamlText = fs.readFileSync(filePath, 'utf8');
|
|
42
|
+
if (yamlText.includes('\n---\n')) {
|
|
43
|
+
const yamlTexts = yamlText.split('\n---\n');
|
|
44
|
+
const config = yaml.load(yamlTexts[0]);
|
|
45
|
+
const testSteps = yaml.load(yamlTexts[1]);
|
|
46
|
+
if (Object.keys(config ?? {}).length > 0) {
|
|
47
|
+
return { config, testSteps };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const testSteps = yaml.load(yamlText);
|
|
51
|
+
if (Object.keys(testSteps).length > 0) {
|
|
52
|
+
return { config: null, testSteps };
|
|
53
|
+
}
|
|
54
|
+
return { config: null, testSteps };
|
|
55
|
+
};
|
|
56
|
+
exports.readTestYamlFileAsJson = readTestYamlFileAsJson;
|
|
57
|
+
async function walk(dir, filterFunction) {
|
|
58
|
+
const readDirResult = await fs.promises.readdir(dir);
|
|
59
|
+
const files = await Promise.all(readDirResult.map(async (file) => {
|
|
60
|
+
const filePath = path.join(dir, file);
|
|
61
|
+
const stats = await fs.promises.stat(filePath);
|
|
62
|
+
if (stats.isDirectory())
|
|
63
|
+
return walk(filePath, filterFunction);
|
|
64
|
+
if (stats.isFile())
|
|
65
|
+
if (filterFunction) {
|
|
66
|
+
if (filterFunction(filePath))
|
|
67
|
+
return filePath;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
return filePath;
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
return files.flat().filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
exports.walk = walk;
|
|
76
|
+
const checkIfFilesExistInWorkspace = (commandName, command, filePath, cleanPath) => {
|
|
77
|
+
const errors = [];
|
|
78
|
+
const files = [];
|
|
79
|
+
const directory = path.dirname(filePath);
|
|
80
|
+
const buildError = (error) => `Flow file "${filePath}" has a command "${commandName}" that references a ${error} "${command}"`;
|
|
81
|
+
const processFilePath = (relativePath) => {
|
|
82
|
+
const absoluteFilePath = path.resolve(directory, relativePath);
|
|
83
|
+
const error = checkFile(absoluteFilePath, cleanPath);
|
|
84
|
+
if (error)
|
|
85
|
+
errors.push(buildError(error));
|
|
86
|
+
files.push(absoluteFilePath.replace(cleanPath, './'));
|
|
87
|
+
};
|
|
88
|
+
// simple command
|
|
89
|
+
if (typeof command === 'string')
|
|
90
|
+
processFilePath(command);
|
|
91
|
+
// array command
|
|
92
|
+
if (Array.isArray(command)) {
|
|
93
|
+
for (const file of command) {
|
|
94
|
+
processFilePath(file);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// object command
|
|
98
|
+
const x = command; // prevent annoying ts error
|
|
99
|
+
if (typeof command === 'object' && x?.file)
|
|
100
|
+
processFilePath(x.file);
|
|
101
|
+
return { errors, files };
|
|
102
|
+
};
|
|
103
|
+
exports.checkIfFilesExistInWorkspace = checkIfFilesExistInWorkspace;
|
|
104
|
+
const checkFile = (filePath, cleanPath) => {
|
|
105
|
+
if (!fs.existsSync(filePath))
|
|
106
|
+
return `non-existent file`;
|
|
107
|
+
if (!filePath.startsWith(cleanPath))
|
|
108
|
+
return `file outside the workspace`;
|
|
109
|
+
};
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
"@oclif/plugin-update": "^4.1.11",
|
|
13
13
|
"@oclif/plugin-warn-if-update-available": "^3.0.10",
|
|
14
14
|
"archiver": "^6.0.1",
|
|
15
|
-
"glob
|
|
16
|
-
"js-yaml": "^4.1.0"
|
|
15
|
+
"glob": "^10.3.10",
|
|
16
|
+
"js-yaml": "^4.1.0",
|
|
17
|
+
"node-stream-zip": "^1.15.0"
|
|
17
18
|
},
|
|
18
19
|
"description": "Better cloud maestro testing",
|
|
19
20
|
"devDependencies": {
|
|
@@ -72,7 +73,7 @@
|
|
|
72
73
|
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
|
|
73
74
|
"version": "oclif readme && git add README.md"
|
|
74
75
|
},
|
|
75
|
-
"version": "0.0.
|
|
76
|
+
"version": "0.0.4",
|
|
76
77
|
"bugs": {
|
|
77
78
|
"url": "https://discord.gg/gm3mJwcNw8"
|
|
78
79
|
},
|