@xano/cli 1.0.6-beta.3 → 1.0.6-beta.6
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/README.md +95 -4
- package/dist/commands/branch/list/index.js +5 -1
- package/dist/commands/ephemeral/list/index.js +2 -1
- package/dist/commands/ephemeral/pull/index.d.ts +0 -6
- package/dist/commands/ephemeral/pull/index.js +10 -95
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
- package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/list/index.js +20 -40
- package/dist/commands/function/list/index.d.ts +4 -4
- package/dist/commands/function/list/index.js +30 -38
- package/dist/commands/knowledge/list/index.js +2 -1
- package/dist/commands/platform/list/index.js +13 -14
- package/dist/commands/release/list/index.js +13 -14
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +10 -95
- package/dist/commands/sandbox/env/list/index.js +4 -0
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +10 -77
- package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/unit_test/list/index.js +9 -14
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +67 -37
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +65 -35
- package/dist/commands/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/static_host/build/list/index.js +19 -40
- package/dist/commands/static_host/build/pull/index.js +12 -9
- package/dist/commands/static_host/list/index.d.ts +2 -3
- package/dist/commands/static_host/list/index.js +20 -40
- package/dist/commands/static_host/migrate/index.js +14 -10
- package/dist/commands/tenant/backup/list/index.js +13 -14
- package/dist/commands/tenant/cluster/list/index.js +9 -10
- package/dist/commands/tenant/env/list/index.js +4 -0
- package/dist/commands/tenant/list/index.js +19 -20
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +7 -142
- package/dist/commands/tenant/snapshot/list/index.js +5 -1
- package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/unit_test/list/index.js +9 -14
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +67 -37
- package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/workflow_test/list/index.js +9 -14
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +65 -35
- package/dist/commands/unit_test/list/index.d.ts +3 -1
- package/dist/commands/unit_test/list/index.js +9 -14
- package/dist/commands/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/unit_test/run_all/index.js +68 -40
- package/dist/commands/workflow_test/list/index.d.ts +3 -1
- package/dist/commands/workflow_test/list/index.js +10 -13
- package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/workflow_test/run_all/index.js +67 -36
- package/dist/commands/workspace/list/index.js +7 -13
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +7 -144
- package/dist/utils/concurrency.d.ts +29 -0
- package/dist/utils/concurrency.js +54 -0
- package/dist/utils/document-parser.d.ts +39 -0
- package/dist/utils/document-parser.js +139 -0
- package/dist/utils/paging.d.ts +141 -0
- package/dist/utils/paging.js +194 -0
- package/oclif.manifest.json +2950 -2824
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { createOrderedEmitter, mapWithConcurrency } from '../../../../utils/concurrency.js';
|
|
4
|
+
import { collectAllPages, INTERNAL_PAGE_SIZE, normalizeListResponse } from '../../../../utils/paging.js';
|
|
3
5
|
export default class TenantUnitTestRunAll extends BaseCommand {
|
|
4
6
|
static description = 'Run all unit tests for a tenant';
|
|
5
7
|
static examples = [
|
|
@@ -21,6 +23,11 @@ Results: 4 passed, 1 failed
|
|
|
21
23
|
description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
|
|
22
24
|
required: false,
|
|
23
25
|
}),
|
|
26
|
+
concurrency: Flags.integer({
|
|
27
|
+
default: 1,
|
|
28
|
+
description: 'Run this many tests in parallel. Tests share the workspace database, so raise this only when your tests do not depend on shared state.',
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
24
31
|
'obj-type': Flags.string({
|
|
25
32
|
description: 'Filter by object type',
|
|
26
33
|
options: ['function', 'query', 'middleware'],
|
|
@@ -57,34 +64,36 @@ Results: 4 passed, 1 failed
|
|
|
57
64
|
// from the same branch.
|
|
58
65
|
const branch = flags.branch ?? profile.branch;
|
|
59
66
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
// Page the list rather than asking for everything at once: 100 per
|
|
68
|
+
// request is plenty for typical suites and does not make the instance
|
|
69
|
+
// build one enormous response. Termination is the server's own nextPage,
|
|
70
|
+
// never "the page came back full".
|
|
71
|
+
const branchValue = branch;
|
|
72
|
+
const tests = await collectAllPages(async (page) => {
|
|
73
|
+
const listParams = new URLSearchParams();
|
|
74
|
+
listParams.set('page', String(page));
|
|
75
|
+
listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
|
|
76
|
+
if (branchValue)
|
|
77
|
+
listParams.set('branch', branchValue);
|
|
78
|
+
if (flags['obj-type'])
|
|
79
|
+
listParams.set('obj_type', flags['obj-type']);
|
|
80
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
81
|
+
headers: {
|
|
82
|
+
accept: 'application/json',
|
|
83
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
84
|
+
},
|
|
85
|
+
method: 'GET',
|
|
86
|
+
}, flags.verbose, profile.access_token);
|
|
87
|
+
if (!listResponse.ok) {
|
|
88
|
+
const errorText = await listResponse.text();
|
|
89
|
+
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
90
|
+
}
|
|
91
|
+
return normalizeListResponse(await listResponse.json());
|
|
92
|
+
}, {
|
|
93
|
+
onTruncate: (collected) => {
|
|
94
|
+
this.warn(`Stopped after ${collected} unit tests — the list did not terminate. Some tests may not have run.`);
|
|
70
95
|
},
|
|
71
|
-
|
|
72
|
-
}, flags.verbose, profile.access_token);
|
|
73
|
-
if (!listResponse.ok) {
|
|
74
|
-
const errorText = await listResponse.text();
|
|
75
|
-
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
76
|
-
}
|
|
77
|
-
const data = (await listResponse.json());
|
|
78
|
-
let tests;
|
|
79
|
-
if (Array.isArray(data)) {
|
|
80
|
-
tests = data;
|
|
81
|
-
}
|
|
82
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
83
|
-
tests = data.items;
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
this.error('Unexpected API response format');
|
|
87
|
-
}
|
|
96
|
+
});
|
|
88
97
|
if (tests.length === 0) {
|
|
89
98
|
this.log('No unit tests found');
|
|
90
99
|
return;
|
|
@@ -93,7 +102,15 @@ Results: 4 passed, 1 failed
|
|
|
93
102
|
this.log(`Running ${tests.length} unit test${tests.length === 1 ? '' : 's'}...\n`);
|
|
94
103
|
}
|
|
95
104
|
const results = [];
|
|
96
|
-
|
|
105
|
+
const runOne = async (test) => {
|
|
106
|
+
// Shadowed accumulators: the body below is the original sequential
|
|
107
|
+
// logic, but it records output instead of printing it so a concurrent
|
|
108
|
+
// run can still emit in input order.
|
|
109
|
+
const results = [];
|
|
110
|
+
const lines = [];
|
|
111
|
+
const log = (message) => {
|
|
112
|
+
lines.push(message);
|
|
113
|
+
};
|
|
97
114
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
98
115
|
try {
|
|
99
116
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
@@ -115,10 +132,10 @@ Results: 4 passed, 1 failed
|
|
|
115
132
|
status: 'fail',
|
|
116
133
|
});
|
|
117
134
|
if (flags.output === 'summary') {
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
136
|
+
log(` Error: API error ${runResponse.status}`);
|
|
120
137
|
}
|
|
121
|
-
|
|
138
|
+
return { lines, results };
|
|
122
139
|
}
|
|
123
140
|
const runResult = (await runResponse.json());
|
|
124
141
|
const passed = runResult.status === 'ok';
|
|
@@ -132,13 +149,13 @@ Results: 4 passed, 1 failed
|
|
|
132
149
|
});
|
|
133
150
|
if (flags.output === 'summary') {
|
|
134
151
|
if (passed) {
|
|
135
|
-
|
|
152
|
+
log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
136
153
|
}
|
|
137
154
|
else {
|
|
138
|
-
|
|
155
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
139
156
|
for (const expect of failedExpects) {
|
|
140
157
|
if (expect.message) {
|
|
141
|
-
|
|
158
|
+
log(` Error: ${expect.message}`);
|
|
142
159
|
}
|
|
143
160
|
}
|
|
144
161
|
}
|
|
@@ -154,11 +171,24 @@ Results: 4 passed, 1 failed
|
|
|
154
171
|
status: 'fail',
|
|
155
172
|
});
|
|
156
173
|
if (flags.output === 'summary') {
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
175
|
+
log(` Error: ${message}`);
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
|
-
|
|
178
|
+
return { lines, results };
|
|
179
|
+
};
|
|
180
|
+
// Print in input order as each test settles, so concurrent output stays
|
|
181
|
+
// diffable against a sequential run.
|
|
182
|
+
const emitter = createOrderedEmitter((settled) => {
|
|
183
|
+
for (const line of settled.lines)
|
|
184
|
+
this.log(line);
|
|
185
|
+
});
|
|
186
|
+
const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
|
|
187
|
+
const settled = await runOne(test);
|
|
188
|
+
emitter.settle(index, settled);
|
|
189
|
+
return settled;
|
|
190
|
+
});
|
|
191
|
+
results.push(...perTest.flatMap((r) => r.results));
|
|
162
192
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
163
193
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
164
194
|
if (flags.output === 'json') {
|
|
@@ -3,10 +3,12 @@ export default class TenantWorkflowTestList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
6
|
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
7
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../../utils/paging.js';
|
|
3
4
|
export default class TenantWorkflowTestList extends BaseCommand {
|
|
4
5
|
static description = 'List workflow tests for a tenant';
|
|
5
6
|
static examples = [
|
|
@@ -23,6 +24,7 @@ Workflow tests for tenant my-tenant:
|
|
|
23
24
|
options: ['summary', 'json'],
|
|
24
25
|
required: false,
|
|
25
26
|
}),
|
|
27
|
+
...pagingFlags('envelope', { maxPerPage: 10_000 }),
|
|
26
28
|
tenant: Flags.string({
|
|
27
29
|
char: 't',
|
|
28
30
|
description: 'Tenant name',
|
|
@@ -42,8 +44,7 @@ Workflow tests for tenant my-tenant:
|
|
|
42
44
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
43
45
|
}
|
|
44
46
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
45
|
-
const params = new URLSearchParams();
|
|
46
|
-
params.set('per_page', '10000');
|
|
47
|
+
const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
|
|
47
48
|
if (flags.branch)
|
|
48
49
|
params.set('branch', flags.branch);
|
|
49
50
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/workflow_test?${params}`;
|
|
@@ -59,19 +60,10 @@ Workflow tests for tenant my-tenant:
|
|
|
59
60
|
const errorText = await response.text();
|
|
60
61
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
61
62
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
if (Array.isArray(data)) {
|
|
65
|
-
tests = data;
|
|
66
|
-
}
|
|
67
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
68
|
-
tests = data.items;
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
this.error('Unexpected API response format');
|
|
72
|
-
}
|
|
63
|
+
const list = normalizeListResponse(await response.json());
|
|
64
|
+
const tests = list.items;
|
|
73
65
|
if (flags.output === 'json') {
|
|
74
|
-
this.log(JSON.stringify(
|
|
66
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
|
|
75
67
|
}
|
|
76
68
|
else {
|
|
77
69
|
if (tests.length === 0) {
|
|
@@ -83,6 +75,9 @@ Workflow tests for tenant my-tenant:
|
|
|
83
75
|
this.log(` - ${test.name} (ID: ${test.id})`);
|
|
84
76
|
}
|
|
85
77
|
}
|
|
78
|
+
const footer = formatPagingFooter(list, { noun: 'workflow test', page: flags.page, tier: 'envelope' });
|
|
79
|
+
if (footer)
|
|
80
|
+
this.log(footer);
|
|
86
81
|
}
|
|
87
82
|
}
|
|
88
83
|
catch (error) {
|
|
@@ -4,6 +4,7 @@ export default class TenantWorkflowTestRunAll extends BaseCommand {
|
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
6
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { createOrderedEmitter, mapWithConcurrency } from '../../../../utils/concurrency.js';
|
|
4
|
+
import { collectAllPages, INTERNAL_PAGE_SIZE, normalizeListResponse } from '../../../../utils/paging.js';
|
|
3
5
|
export default class TenantWorkflowTestRunAll extends BaseCommand {
|
|
4
6
|
static description = 'Run all workflow tests for a tenant';
|
|
5
7
|
static examples = [
|
|
@@ -21,6 +23,11 @@ Results: 2 passed, 1 failed
|
|
|
21
23
|
description: 'Filter by branch name',
|
|
22
24
|
required: false,
|
|
23
25
|
}),
|
|
26
|
+
concurrency: Flags.integer({
|
|
27
|
+
default: 1,
|
|
28
|
+
description: 'Run this many tests in parallel. Tests share the workspace database, so raise this only when your tests do not depend on shared state.',
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
24
31
|
output: Flags.string({
|
|
25
32
|
char: 'o',
|
|
26
33
|
default: 'summary',
|
|
@@ -49,32 +56,34 @@ Results: 2 passed, 1 failed
|
|
|
49
56
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
50
57
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/workflow_test`;
|
|
51
58
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
// Page the list rather than asking for everything at once: 100 per
|
|
60
|
+
// request is plenty for typical suites and does not make the instance
|
|
61
|
+
// build one enormous response. Termination is the server's own nextPage,
|
|
62
|
+
// never "the page came back full".
|
|
63
|
+
const branchValue = flags.branch;
|
|
64
|
+
const tests = await collectAllPages(async (page) => {
|
|
65
|
+
const listParams = new URLSearchParams();
|
|
66
|
+
listParams.set('page', String(page));
|
|
67
|
+
listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
|
|
68
|
+
if (branchValue)
|
|
69
|
+
listParams.set('branch', branchValue);
|
|
70
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
71
|
+
headers: {
|
|
72
|
+
accept: 'application/json',
|
|
73
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
74
|
+
},
|
|
75
|
+
method: 'GET',
|
|
76
|
+
}, flags.verbose, profile.access_token);
|
|
77
|
+
if (!listResponse.ok) {
|
|
78
|
+
const errorText = await listResponse.text();
|
|
79
|
+
this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
80
|
+
}
|
|
81
|
+
return normalizeListResponse(await listResponse.json());
|
|
82
|
+
}, {
|
|
83
|
+
onTruncate: (collected) => {
|
|
84
|
+
this.warn(`Stopped after ${collected} workflow tests — the list did not terminate. Some tests may not have run.`);
|
|
60
85
|
},
|
|
61
|
-
|
|
62
|
-
}, flags.verbose, profile.access_token);
|
|
63
|
-
if (!listResponse.ok) {
|
|
64
|
-
const errorText = await listResponse.text();
|
|
65
|
-
this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
66
|
-
}
|
|
67
|
-
const data = (await listResponse.json());
|
|
68
|
-
let tests;
|
|
69
|
-
if (Array.isArray(data)) {
|
|
70
|
-
tests = data;
|
|
71
|
-
}
|
|
72
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
73
|
-
tests = data.items;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
this.error('Unexpected API response format');
|
|
77
|
-
}
|
|
86
|
+
});
|
|
78
87
|
if (tests.length === 0) {
|
|
79
88
|
this.log('No workflow tests found');
|
|
80
89
|
return;
|
|
@@ -83,7 +92,15 @@ Results: 2 passed, 1 failed
|
|
|
83
92
|
this.log(`Running ${tests.length} workflow test${tests.length === 1 ? '' : 's'}...\n`);
|
|
84
93
|
}
|
|
85
94
|
const results = [];
|
|
86
|
-
|
|
95
|
+
const runOne = async (test) => {
|
|
96
|
+
// Shadowed accumulators: the body below is the original sequential
|
|
97
|
+
// logic, but it records output instead of printing it so a concurrent
|
|
98
|
+
// run can still emit in input order.
|
|
99
|
+
const results = [];
|
|
100
|
+
const lines = [];
|
|
101
|
+
const log = (message) => {
|
|
102
|
+
lines.push(message);
|
|
103
|
+
};
|
|
87
104
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
88
105
|
try {
|
|
89
106
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
@@ -102,10 +119,10 @@ Results: 2 passed, 1 failed
|
|
|
102
119
|
status: 'fail',
|
|
103
120
|
});
|
|
104
121
|
if (flags.output === 'summary') {
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
log(`FAIL ${test.name}`);
|
|
123
|
+
log(` Error: API error ${runResponse.status}`);
|
|
107
124
|
}
|
|
108
|
-
|
|
125
|
+
return { lines, results };
|
|
109
126
|
}
|
|
110
127
|
const runResult = (await runResponse.json());
|
|
111
128
|
const passed = runResult.status === 'ok';
|
|
@@ -118,12 +135,12 @@ Results: 2 passed, 1 failed
|
|
|
118
135
|
if (flags.output === 'summary') {
|
|
119
136
|
const timing = runResult.timing ? ` (${runResult.timing}s)` : '';
|
|
120
137
|
if (passed) {
|
|
121
|
-
|
|
138
|
+
log(`PASS ${test.name}${timing}`);
|
|
122
139
|
}
|
|
123
140
|
else {
|
|
124
|
-
|
|
141
|
+
log(`FAIL ${test.name}${timing}`);
|
|
125
142
|
if (runResult.message) {
|
|
126
|
-
|
|
143
|
+
log(` Error: ${runResult.message}`);
|
|
127
144
|
}
|
|
128
145
|
}
|
|
129
146
|
}
|
|
@@ -136,11 +153,24 @@ Results: 2 passed, 1 failed
|
|
|
136
153
|
status: 'fail',
|
|
137
154
|
});
|
|
138
155
|
if (flags.output === 'summary') {
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
log(`FAIL ${test.name}`);
|
|
157
|
+
log(` Error: ${message}`);
|
|
141
158
|
}
|
|
142
159
|
}
|
|
143
|
-
|
|
160
|
+
return { lines, results };
|
|
161
|
+
};
|
|
162
|
+
// Print in input order as each test settles, so concurrent output stays
|
|
163
|
+
// diffable against a sequential run.
|
|
164
|
+
const emitter = createOrderedEmitter((settled) => {
|
|
165
|
+
for (const line of settled.lines)
|
|
166
|
+
this.log(line);
|
|
167
|
+
});
|
|
168
|
+
const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
|
|
169
|
+
const settled = await runOne(test);
|
|
170
|
+
emitter.settle(index, settled);
|
|
171
|
+
return settled;
|
|
172
|
+
});
|
|
173
|
+
results.push(...perTest.flatMap((r) => r.results));
|
|
144
174
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
145
175
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
146
176
|
if (flags.output === 'json') {
|
|
@@ -3,10 +3,12 @@ export default class UnitTestList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
10
|
'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
11
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../utils/paging.js';
|
|
3
4
|
export default class UnitTestList extends BaseCommand {
|
|
4
5
|
static description = 'List all unit tests in a workspace';
|
|
5
6
|
static examples = [
|
|
@@ -30,6 +31,7 @@ Unit tests in workspace 5:
|
|
|
30
31
|
options: ['summary', 'json'],
|
|
31
32
|
required: false,
|
|
32
33
|
}),
|
|
34
|
+
...pagingFlags('envelope', { maxPerPage: 10_000 }),
|
|
33
35
|
workspace: Flags.string({
|
|
34
36
|
char: 'w',
|
|
35
37
|
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
@@ -43,8 +45,7 @@ Unit tests in workspace 5:
|
|
|
43
45
|
if (!workspaceId) {
|
|
44
46
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
45
47
|
}
|
|
46
|
-
const params = new URLSearchParams();
|
|
47
|
-
params.set('per_page', '10000');
|
|
48
|
+
const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
|
|
48
49
|
if (flags.branch) {
|
|
49
50
|
params.set('branch', flags.branch);
|
|
50
51
|
}
|
|
@@ -64,19 +65,10 @@ Unit tests in workspace 5:
|
|
|
64
65
|
const errorText = await response.text();
|
|
65
66
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
66
67
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
if (Array.isArray(data)) {
|
|
70
|
-
tests = data;
|
|
71
|
-
}
|
|
72
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
73
|
-
tests = data.items;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
this.error('Unexpected API response format');
|
|
77
|
-
}
|
|
68
|
+
const list = normalizeListResponse(await response.json());
|
|
69
|
+
const tests = list.items;
|
|
78
70
|
if (flags.output === 'json') {
|
|
79
|
-
this.log(JSON.stringify(
|
|
71
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
|
|
80
72
|
}
|
|
81
73
|
else {
|
|
82
74
|
if (tests.length === 0) {
|
|
@@ -88,6 +80,9 @@ Unit tests in workspace 5:
|
|
|
88
80
|
this.log(` - ${test.name} (ID: ${test.id}) [${test.obj_type}: ${test.obj_name}]`);
|
|
89
81
|
}
|
|
90
82
|
}
|
|
83
|
+
const footer = formatPagingFooter(list, { noun: 'unit test', page: flags.page, tier: 'envelope' });
|
|
84
|
+
if (footer)
|
|
85
|
+
this.log(footer);
|
|
91
86
|
}
|
|
92
87
|
}
|
|
93
88
|
catch (error) {
|
|
@@ -4,6 +4,7 @@ export default class UnitTestRunAll extends BaseCommand {
|
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
6
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { createOrderedEmitter, mapWithConcurrency } from '../../../utils/concurrency.js';
|
|
4
|
+
import { collectAllPages, INTERNAL_PAGE_SIZE, normalizeListResponse } from '../../../utils/paging.js';
|
|
3
5
|
export default class UnitTestRunAll extends BaseCommand {
|
|
4
6
|
static description = 'Run all unit tests in a workspace';
|
|
5
7
|
static examples = [
|
|
@@ -22,6 +24,11 @@ Results: 2 passed, 1 failed
|
|
|
22
24
|
description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
|
|
23
25
|
required: false,
|
|
24
26
|
}),
|
|
27
|
+
concurrency: Flags.integer({
|
|
28
|
+
default: 1,
|
|
29
|
+
description: 'Run this many tests in parallel. Tests share the workspace database, so raise this only when your tests do not depend on shared state.',
|
|
30
|
+
required: false,
|
|
31
|
+
}),
|
|
25
32
|
'obj-type': Flags.string({
|
|
26
33
|
description: 'Filter by object type',
|
|
27
34
|
options: ['function', 'query', 'middleware'],
|
|
@@ -53,36 +60,36 @@ Results: 2 passed, 1 failed
|
|
|
53
60
|
const branch = flags.branch ?? profile.branch;
|
|
54
61
|
try {
|
|
55
62
|
// Step 1: List all unit tests
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
listParams
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
// Page the list rather than asking for everything at once: 100 per
|
|
64
|
+
// request is plenty for typical suites and does not make the instance
|
|
65
|
+
// build one enormous response. Termination is the server's own nextPage,
|
|
66
|
+
// never "the page came back full".
|
|
67
|
+
const branchValue = branch;
|
|
68
|
+
const tests = await collectAllPages(async (page) => {
|
|
69
|
+
const listParams = new URLSearchParams();
|
|
70
|
+
listParams.set('page', String(page));
|
|
71
|
+
listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
|
|
72
|
+
if (branchValue)
|
|
73
|
+
listParams.set('branch', branchValue);
|
|
74
|
+
if (flags['obj-type'])
|
|
75
|
+
listParams.set('obj_type', flags['obj-type']);
|
|
76
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
77
|
+
headers: {
|
|
78
|
+
accept: 'application/json',
|
|
79
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
80
|
+
},
|
|
81
|
+
method: 'GET',
|
|
82
|
+
}, flags.verbose, profile.access_token);
|
|
83
|
+
if (!listResponse.ok) {
|
|
84
|
+
const errorText = await listResponse.text();
|
|
85
|
+
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
86
|
+
}
|
|
87
|
+
return normalizeListResponse(await listResponse.json());
|
|
88
|
+
}, {
|
|
89
|
+
onTruncate: (collected) => {
|
|
90
|
+
this.warn(`Stopped after ${collected} unit tests — the list did not terminate. Some tests may not have run.`);
|
|
68
91
|
},
|
|
69
|
-
|
|
70
|
-
}, flags.verbose, profile.access_token);
|
|
71
|
-
if (!listResponse.ok) {
|
|
72
|
-
const errorText = await listResponse.text();
|
|
73
|
-
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
74
|
-
}
|
|
75
|
-
const data = (await listResponse.json());
|
|
76
|
-
let tests;
|
|
77
|
-
if (Array.isArray(data)) {
|
|
78
|
-
tests = data;
|
|
79
|
-
}
|
|
80
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
81
|
-
tests = data.items;
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
this.error('Unexpected API response format');
|
|
85
|
-
}
|
|
92
|
+
});
|
|
86
93
|
if (tests.length === 0) {
|
|
87
94
|
this.log('No unit tests found');
|
|
88
95
|
return;
|
|
@@ -92,7 +99,15 @@ Results: 2 passed, 1 failed
|
|
|
92
99
|
}
|
|
93
100
|
// Step 2: Run each test
|
|
94
101
|
const results = [];
|
|
95
|
-
|
|
102
|
+
const runOne = async (test) => {
|
|
103
|
+
// Shadowed accumulators: the body below is the original sequential
|
|
104
|
+
// logic, but it records output instead of printing it so a concurrent
|
|
105
|
+
// run can still emit in input order.
|
|
106
|
+
const results = [];
|
|
107
|
+
const lines = [];
|
|
108
|
+
const log = (message) => {
|
|
109
|
+
lines.push(message);
|
|
110
|
+
};
|
|
96
111
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
97
112
|
try {
|
|
98
113
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
@@ -115,10 +130,10 @@ Results: 2 passed, 1 failed
|
|
|
115
130
|
};
|
|
116
131
|
results.push(result);
|
|
117
132
|
if (flags.output === 'summary') {
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
134
|
+
log(` Error: API error ${runResponse.status}`);
|
|
120
135
|
}
|
|
121
|
-
|
|
136
|
+
return { lines, results };
|
|
122
137
|
}
|
|
123
138
|
const runResult = (await runResponse.json());
|
|
124
139
|
const passed = runResult.status === 'ok';
|
|
@@ -133,13 +148,13 @@ Results: 2 passed, 1 failed
|
|
|
133
148
|
results.push(result);
|
|
134
149
|
if (flags.output === 'summary') {
|
|
135
150
|
if (passed) {
|
|
136
|
-
|
|
151
|
+
log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
137
152
|
}
|
|
138
153
|
else {
|
|
139
|
-
|
|
154
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
140
155
|
for (const expect of failedExpects) {
|
|
141
156
|
if (expect.message) {
|
|
142
|
-
|
|
157
|
+
log(` Error: ${expect.message}`);
|
|
143
158
|
}
|
|
144
159
|
}
|
|
145
160
|
}
|
|
@@ -155,16 +170,29 @@ Results: 2 passed, 1 failed
|
|
|
155
170
|
status: 'fail',
|
|
156
171
|
});
|
|
157
172
|
if (flags.output === 'summary') {
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
174
|
+
log(` Error: ${message}`);
|
|
160
175
|
}
|
|
161
176
|
}
|
|
162
|
-
|
|
177
|
+
return { lines, results };
|
|
178
|
+
};
|
|
179
|
+
// Print in input order as each test settles, so concurrent output stays
|
|
180
|
+
// diffable against a sequential run.
|
|
181
|
+
const emitter = createOrderedEmitter((settled) => {
|
|
182
|
+
for (const line of settled.lines)
|
|
183
|
+
this.log(line);
|
|
184
|
+
});
|
|
185
|
+
const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
|
|
186
|
+
const settled = await runOne(test);
|
|
187
|
+
emitter.settle(index, settled);
|
|
188
|
+
return settled;
|
|
189
|
+
});
|
|
190
|
+
results.push(...perTest.flatMap((r) => r.results));
|
|
163
191
|
// Step 3: Summary
|
|
164
192
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
165
193
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
166
194
|
if (flags.output === 'json') {
|
|
167
|
-
this.log(JSON.stringify({
|
|
195
|
+
this.log(JSON.stringify({ failed, passed, results }, null, 2));
|
|
168
196
|
}
|
|
169
197
|
else {
|
|
170
198
|
this.log(`\nResults: ${passed} passed, ${failed} failed`);
|