@typespec/http-client-python 0.28.3 → 0.29.0
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/emitter/code-model.d.ts.map +1 -1
- package/dist/emitter/code-model.js +1 -0
- package/dist/emitter/code-model.js.map +1 -1
- package/dist/emitter/constants.d.ts +6 -0
- package/dist/emitter/constants.d.ts.map +1 -0
- package/dist/emitter/constants.js +27 -0
- package/dist/emitter/constants.js.map +1 -0
- package/dist/emitter/emitter.d.ts.map +1 -1
- package/dist/emitter/emitter.js +169 -94
- package/dist/emitter/emitter.js.map +1 -1
- package/dist/emitter/http.d.ts.map +1 -1
- package/dist/emitter/http.js +33 -1
- package/dist/emitter/http.js.map +1 -1
- package/dist/emitter/lib.d.ts +33 -2
- package/dist/emitter/lib.d.ts.map +1 -1
- package/dist/emitter/lib.js +17 -0
- package/dist/emitter/lib.js.map +1 -1
- package/dist/emitter/run-python3.js +1 -1
- package/dist/emitter/run-python3.js.map +1 -1
- package/dist/emitter/types.d.ts.map +1 -1
- package/dist/emitter/types.js +31 -5
- package/dist/emitter/types.js.map +1 -1
- package/emitter/src/code-model.ts +1 -0
- package/emitter/src/constants.ts +27 -0
- package/emitter/src/emitter.ts +216 -96
- package/emitter/src/http.ts +29 -1
- package/emitter/src/lib.ts +20 -0
- package/emitter/src/run-python3.ts +1 -1
- package/emitter/src/types.ts +35 -5
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/emitter/test/utils.test.ts +3 -0
- package/emitter/tsconfig.build.json +2 -1
- package/emitter/tsconfig.json +2 -1
- package/eng/scripts/ci/config/eslint-ci.config.mjs +10 -0
- package/eng/scripts/ci/config/mypy.ini +1 -1
- package/eng/scripts/ci/config/pylintrc +1 -1
- package/eng/scripts/ci/config/pyrightconfig.json +1 -1
- package/eng/scripts/ci/dev_requirements.txt +1 -1
- package/eng/scripts/ci/lint.ts +11 -4
- package/eng/scripts/ci/regenerate-common.ts +407 -136
- package/eng/scripts/ci/regenerate.ts +103 -194
- package/eng/scripts/ci/run-tests.ts +153 -48
- package/eng/scripts/ci/run_apiview.py +22 -16
- package/eng/scripts/ci/run_mypy.py +28 -18
- package/eng/scripts/ci/run_pylint.py +23 -16
- package/eng/scripts/ci/run_pyright.py +5 -2
- package/eng/scripts/ci/run_sphinx_build.py +24 -9
- package/eng/scripts/ci/util.py +61 -25
- package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
- package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
- package/eng/scripts/setup/build_pygen_wheel.py +3 -3
- package/eng/scripts/setup/install.py +3 -3
- package/eng/scripts/setup/install.ts +1 -1
- package/eng/scripts/setup/prepare.py +2 -2
- package/eng/scripts/setup/run-python3.ts +1 -1
- package/eng/scripts/setup/run_batch.py +197 -0
- package/eng/scripts/setup/venvtools.py +0 -1
- package/generator/build/lib/pygen/__init__.py +1 -3
- package/generator/build/lib/pygen/codegen/__init__.py +0 -1
- package/generator/build/lib/pygen/codegen/models/base.py +0 -1
- package/generator/build/lib/pygen/codegen/models/client.py +2 -1
- package/generator/build/lib/pygen/codegen/models/code_model.py +1 -0
- package/generator/build/lib/pygen/codegen/models/enum_type.py +0 -1
- package/generator/build/lib/pygen/codegen/models/model_type.py +1 -7
- package/generator/build/lib/pygen/codegen/models/parameter.py +16 -14
- package/generator/build/lib/pygen/codegen/serializers/__init__.py +27 -12
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +12 -3
- package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +6 -2
- package/generator/build/lib/pygen/codegen/serializers/import_serializer.py +17 -4
- package/generator/build/lib/pygen/codegen/serializers/parameter_serializer.py +17 -11
- package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +19 -7
- package/generator/build/lib/pygen/codegen/templates/packaging_templates/README.md.jinja2 +11 -3
- package/generator/build/lib/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +1 -1
- package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +5 -1
- package/generator/build/lib/pygen/preprocess/__init__.py +37 -30
- package/generator/component-detection-pip-report.json +7 -7
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/__init__.py +1 -3
- package/generator/pygen/codegen/__init__.py +0 -1
- package/generator/pygen/codegen/models/base.py +0 -1
- package/generator/pygen/codegen/models/client.py +2 -1
- package/generator/pygen/codegen/models/code_model.py +1 -0
- package/generator/pygen/codegen/models/enum_type.py +0 -1
- package/generator/pygen/codegen/models/model_type.py +1 -7
- package/generator/pygen/codegen/models/parameter.py +16 -14
- package/generator/pygen/codegen/serializers/__init__.py +27 -12
- package/generator/pygen/codegen/serializers/builder_serializer.py +12 -3
- package/generator/pygen/codegen/serializers/general_serializer.py +6 -2
- package/generator/pygen/codegen/serializers/import_serializer.py +17 -4
- package/generator/pygen/codegen/serializers/parameter_serializer.py +17 -11
- package/generator/pygen/codegen/templates/model_base.py.jinja2 +19 -7
- package/generator/pygen/codegen/templates/packaging_templates/README.md.jinja2 +11 -3
- package/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +1 -1
- package/generator/pygen/codegen/templates/serialization.py.jinja2 +5 -1
- package/generator/pygen/preprocess/__init__.py +37 -30
- package/generator/pygen.egg-info/PKG-INFO +1 -2
- package/generator/pygen.egg-info/requires.txt +1 -1
- package/generator/setup.py +1 -2
- package/package.json +24 -9
|
@@ -2,24 +2,32 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Regenerates Python SDK code from TypeSpec definitions.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Two-phase pipeline:
|
|
6
|
+
* 1. TypeSpec compile (in-process, parallel) -> emits per-spec YAML only.
|
|
7
|
+
* 2. Single batched Python subprocess reads all YAMLs and writes the
|
|
8
|
+
* final `.py` files. Amortizes Python-startup cost across many specs.
|
|
9
|
+
*
|
|
10
|
+
* Shared helpers/data live in `regenerate-common.ts` (kept identical with the
|
|
11
|
+
* `@azure-tools/typespec-python` wrapper copy).
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
import { access, readdir } from "fs/promises";
|
|
11
17
|
import { platform } from "os";
|
|
12
|
-
import { dirname, join,
|
|
18
|
+
import { dirname, join, resolve } from "path";
|
|
13
19
|
import pc from "picocolors";
|
|
14
20
|
import { fileURLToPath } from "url";
|
|
15
21
|
import { parseArgs } from "util";
|
|
22
|
+
|
|
16
23
|
import {
|
|
17
|
-
|
|
18
|
-
BASE_EMITTER_OPTIONS,
|
|
24
|
+
buildTaskGroups,
|
|
19
25
|
getSubdirectories,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
prepareBaselineOfGeneratedCode,
|
|
27
|
+
preprocess,
|
|
28
|
+
RegenerateContext,
|
|
29
|
+
RegenerateFlags,
|
|
30
|
+
runParallel,
|
|
23
31
|
} from "./regenerate-common.js";
|
|
24
32
|
|
|
25
33
|
// Parse arguments
|
|
@@ -93,193 +101,66 @@ const GENERATED_FOLDER = argv.values.generatedFolder
|
|
|
93
101
|
: resolve(PLUGIN_DIR, "generator");
|
|
94
102
|
const EMITTER_NAME = argv.values.emitterName || "@typespec/http-client-python";
|
|
95
103
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<string, string>[]> = {
|
|
106
|
-
...BASE_EMITTER_OPTIONS,
|
|
107
|
-
"type/array": {
|
|
108
|
-
"package-name": "typetest-array",
|
|
109
|
-
namespace: "typetest.array",
|
|
110
|
-
},
|
|
111
|
-
"type/model/inheritance/recursive": {
|
|
112
|
-
"package-name": "typetest-model-recursive",
|
|
113
|
-
namespace: "typetest.model.recursive",
|
|
114
|
-
},
|
|
104
|
+
const ctx: RegenerateContext = {
|
|
105
|
+
pluginDir: PLUGIN_DIR,
|
|
106
|
+
azureHttpSpecs: AZURE_HTTP_SPECS,
|
|
107
|
+
httpSpecs: HTTP_SPECS,
|
|
108
|
+
generatedFolder: GENERATED_FOLDER,
|
|
109
|
+
emitterName: EMITTER_NAME,
|
|
115
110
|
};
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
outputDir: string;
|
|
120
|
-
options: Record<string, unknown>;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Group of tasks for the same spec that must run sequentially
|
|
124
|
-
interface TaskGroup {
|
|
125
|
-
spec: string;
|
|
126
|
-
tasks: CompileTask[];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function defaultPackageName(spec: string): string {
|
|
130
|
-
const specDir = spec.includes("azure") ? AZURE_HTTP_SPECS : HTTP_SPECS;
|
|
131
|
-
return toPosix(relative(specDir, dirname(spec)))
|
|
132
|
-
.replace(/\//g, "-")
|
|
133
|
-
.toLowerCase();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getEmitterOptions(spec: string, flavor: string): Record<string, string>[] {
|
|
137
|
-
const specDir = spec.includes("azure") ? AZURE_HTTP_SPECS : HTTP_SPECS;
|
|
138
|
-
const relativeSpec = toPosix(relative(specDir, spec));
|
|
139
|
-
const key = relativeSpec.includes("resiliency/srv-driven/old.tsp")
|
|
140
|
-
? relativeSpec
|
|
141
|
-
: dirname(relativeSpec);
|
|
142
|
-
const emitterOpts = EMITTER_OPTIONS[key] ||
|
|
143
|
-
(flavor === "azure" ? AZURE_EMITTER_OPTIONS[key] : [{}]) || [{}];
|
|
144
|
-
return Array.isArray(emitterOpts) ? emitterOpts : [emitterOpts];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function buildTaskGroups(specs: string[], flags: RegenerateFlags): TaskGroup[] {
|
|
148
|
-
const groups: TaskGroup[] = [];
|
|
149
|
-
|
|
150
|
-
for (const spec of specs) {
|
|
151
|
-
const tasks: CompileTask[] = [];
|
|
152
|
-
|
|
153
|
-
for (const emitterConfig of getEmitterOptions(spec, flags.flavor)) {
|
|
154
|
-
const options: Record<string, unknown> = { ...emitterConfig };
|
|
155
|
-
|
|
156
|
-
// Add flavor-specific options
|
|
157
|
-
options["flavor"] = flags.flavor;
|
|
158
|
-
for (const [k, v] of Object.entries(SpecialFlags[flags.flavor] ?? {})) {
|
|
159
|
-
options[k] = v;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Set output directory - use tests/generated/<flavor>/<package> structure
|
|
163
|
-
const packageName = (options["package-name"] as string) || defaultPackageName(spec);
|
|
164
|
-
const outputDir =
|
|
165
|
-
(options["emitter-output-dir"] as string) ||
|
|
166
|
-
toPosix(`${GENERATED_FOLDER}/../tests/generated/${flags.flavor}/${packageName}`);
|
|
167
|
-
options["emitter-output-dir"] = outputDir;
|
|
168
|
-
|
|
169
|
-
// Debug mode
|
|
170
|
-
if (flags.debug) {
|
|
171
|
-
options["debug"] = true;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Examples directory
|
|
175
|
-
options["examples-dir"] = toPosix(join(dirname(spec), "examples"));
|
|
176
|
-
|
|
177
|
-
tasks.push({ spec, outputDir, options });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
groups.push({ spec, tasks });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return groups;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function compileSpec(task: CompileTask): Promise<{ success: boolean; error?: string }> {
|
|
187
|
-
const { spec, outputDir, options } = task;
|
|
188
|
-
|
|
112
|
+
async function collectConfigFiles(generatedDir: string, flavor: string): Promise<string[]> {
|
|
113
|
+
const flavorDir = join(generatedDir, "..", "tests", "generated", flavor);
|
|
189
114
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
options: {
|
|
194
|
-
[EMITTER_NAME]: options,
|
|
195
|
-
},
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
// Compile using TypeSpec compiler directly (no subprocess)
|
|
199
|
-
const program = await compile(NodeHost, spec, compilerOptions);
|
|
200
|
-
|
|
201
|
-
if (program.hasError()) {
|
|
202
|
-
const errors = program.diagnostics
|
|
203
|
-
.filter((d) => d.severity === "error")
|
|
204
|
-
.map((d) => d.message)
|
|
205
|
-
.join("\n");
|
|
206
|
-
return { success: false, error: errors };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return { success: true };
|
|
210
|
-
} catch (err) {
|
|
211
|
-
// Clean up on error
|
|
212
|
-
rmSync(outputDir, { recursive: true, force: true });
|
|
213
|
-
return { success: false, error: String(err) };
|
|
115
|
+
await access(flavorDir);
|
|
116
|
+
} catch {
|
|
117
|
+
return [];
|
|
214
118
|
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function runParallel(groups: TaskGroup[], maxJobs: number): Promise<Map<string, boolean>> {
|
|
218
|
-
const results = new Map<string, boolean>();
|
|
219
|
-
const executing: Set<Promise<void>> = new Set();
|
|
220
119
|
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const runGroup = async () => {
|
|
229
|
-
const specDir = group.spec.includes("azure") ? AZURE_HTTP_SPECS : HTTP_SPECS;
|
|
230
|
-
const shortName = toPosix(relative(specDir, dirname(group.spec)));
|
|
231
|
-
|
|
232
|
-
// Run all tasks in this group sequentially to avoid state pollution
|
|
233
|
-
let groupSuccess = true;
|
|
234
|
-
for (const task of group.tasks) {
|
|
235
|
-
const packageName = (task.options["package-name"] as string) || shortName;
|
|
236
|
-
console.log(pc.blue(`[${completed + 1}/${totalTasks}] Compiling ${packageName}...`));
|
|
237
|
-
|
|
238
|
-
const result = await compileSpec(task);
|
|
239
|
-
completed++;
|
|
240
|
-
|
|
241
|
-
if (result.success) {
|
|
242
|
-
console.log(pc.green(`[${completed}/${totalTasks}] ${packageName} succeeded`));
|
|
243
|
-
} else {
|
|
244
|
-
console.log(
|
|
245
|
-
pc.red(`[${completed}/${totalTasks}] ${packageName} failed: ${result.error}`),
|
|
246
|
-
);
|
|
247
|
-
groupSuccess = false;
|
|
120
|
+
const configFiles: string[] = [];
|
|
121
|
+
for (const pkg of await readdir(flavorDir, { withFileTypes: true })) {
|
|
122
|
+
if (pkg.isDirectory()) {
|
|
123
|
+
const pkgDir = join(flavorDir, pkg.name);
|
|
124
|
+
for (const file of await readdir(pkgDir)) {
|
|
125
|
+
if (file.startsWith(".tsp-codegen-") && file.endsWith(".json")) {
|
|
126
|
+
configFiles.push(join(pkgDir, file));
|
|
248
127
|
}
|
|
249
128
|
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return configFiles;
|
|
132
|
+
}
|
|
250
133
|
|
|
251
|
-
|
|
252
|
-
|
|
134
|
+
function runBatchPythonProcessing(flavor: string, configCount: number, jobs: number): boolean {
|
|
135
|
+
if (configCount === 0) return true;
|
|
253
136
|
|
|
254
|
-
|
|
255
|
-
executing.add(p);
|
|
137
|
+
console.log(pc.cyan(`\nRunning batch Python processing on ${configCount} specs...`));
|
|
256
138
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
139
|
+
// Find Python venv
|
|
140
|
+
let venvPath = join(PLUGIN_DIR, "venv");
|
|
141
|
+
if (existsSync(join(venvPath, "bin"))) {
|
|
142
|
+
venvPath = join(venvPath, "bin", "python");
|
|
143
|
+
} else if (existsSync(join(venvPath, "Scripts"))) {
|
|
144
|
+
venvPath = join(venvPath, "Scripts", "python.exe");
|
|
145
|
+
} else {
|
|
146
|
+
console.error(pc.red("Python venv not found"));
|
|
147
|
+
return false;
|
|
260
148
|
}
|
|
261
149
|
|
|
262
|
-
|
|
263
|
-
return results;
|
|
264
|
-
}
|
|
150
|
+
const batchScript = join(PLUGIN_DIR, "eng", "scripts", "setup", "run_batch.py");
|
|
265
151
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"authentication",
|
|
275
|
-
"apikey",
|
|
276
|
-
"_operations",
|
|
277
|
-
];
|
|
278
|
-
await promises.mkdir(join(testsGeneratedDir, ...folderParts), { recursive: true });
|
|
279
|
-
await promises.writeFile(
|
|
280
|
-
join(testsGeneratedDir, ...folderParts, "to_be_deleted.py"),
|
|
281
|
-
"# This file is to be deleted after regeneration",
|
|
152
|
+
try {
|
|
153
|
+
// Pass directory and flavor instead of individual config files to avoid command line length limits on Windows
|
|
154
|
+
execSync(
|
|
155
|
+
`"${venvPath}" "${batchScript}" --generated-dir "${PLUGIN_DIR}" --flavor ${flavor} --jobs ${jobs}`,
|
|
156
|
+
{
|
|
157
|
+
stdio: "inherit",
|
|
158
|
+
cwd: PLUGIN_DIR,
|
|
159
|
+
},
|
|
282
160
|
);
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
283
164
|
}
|
|
284
165
|
}
|
|
285
166
|
|
|
@@ -296,35 +177,61 @@ async function regenerateFlavor(
|
|
|
296
177
|
const flags: RegenerateFlags = { flavor, debug, name };
|
|
297
178
|
|
|
298
179
|
// Preprocess
|
|
299
|
-
await preprocess(flavor);
|
|
180
|
+
await preprocess(flavor, GENERATED_FOLDER);
|
|
300
181
|
|
|
301
182
|
// Collect specs
|
|
302
183
|
const azureSpecs = flavor === "azure" ? await getSubdirectories(AZURE_HTTP_SPECS, flags) : [];
|
|
303
184
|
const standardSpecs = await getSubdirectories(HTTP_SPECS, flags);
|
|
304
185
|
const allSpecs = [...azureSpecs, ...standardSpecs];
|
|
305
186
|
|
|
306
|
-
// Build task groups (tasks for same spec run sequentially to avoid state pollution)
|
|
307
|
-
|
|
187
|
+
// Build task groups (tasks for same spec run sequentially to avoid state pollution).
|
|
188
|
+
// emitYamlOnly: true -> phase 1 emits YAML only; phase 2 (runBatchPythonProcessing) writes .py files.
|
|
189
|
+
const groups = buildTaskGroups(allSpecs, flags, ctx, { emitYamlOnly: true });
|
|
308
190
|
const totalTasks = groups.reduce((sum, g) => sum + g.tasks.length, 0);
|
|
309
191
|
|
|
310
192
|
console.log(pc.cyan(`Found ${allSpecs.length} specs (${totalTasks} total tasks) to compile`));
|
|
311
193
|
console.log(pc.cyan(`Using ${jobs} parallel jobs\n`));
|
|
312
194
|
|
|
313
|
-
// Run compilation
|
|
195
|
+
// Run compilation (emits YAML only)
|
|
314
196
|
const startTime = performance.now();
|
|
315
|
-
const results = await runParallel(groups, jobs);
|
|
316
|
-
const
|
|
197
|
+
const results = await runParallel(groups, jobs, ctx);
|
|
198
|
+
const compileTime = (performance.now() - startTime) / 1000;
|
|
317
199
|
|
|
318
|
-
// Summary
|
|
200
|
+
// Summary for TypeSpec compilation
|
|
319
201
|
const succeeded = Array.from(results.values()).filter((v) => v).length;
|
|
320
|
-
const
|
|
202
|
+
const compileFailed = results.size - succeeded;
|
|
203
|
+
|
|
204
|
+
console.log(
|
|
205
|
+
pc.cyan(
|
|
206
|
+
`\nTypeSpec compilation: ${succeeded} succeeded, ${compileFailed} failed (${compileTime.toFixed(1)}s)`,
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (compileFailed > 0) {
|
|
211
|
+
console.log(pc.red(`Skipping Python processing due to compilation failures`));
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Batch process all specs with Python
|
|
216
|
+
const pyStartTime = performance.now();
|
|
217
|
+
const configCount = (await collectConfigFiles(GENERATED_FOLDER, flavor)).length;
|
|
218
|
+
// Use fewer Python jobs since Python processing is heavier
|
|
219
|
+
const pyJobs = Math.max(4, jobs);
|
|
220
|
+
const pySuccess = runBatchPythonProcessing(flavor, configCount, pyJobs);
|
|
221
|
+
const pyTime = (performance.now() - pyStartTime) / 1000;
|
|
222
|
+
|
|
223
|
+
const totalTime = (performance.now() - startTime) / 1000;
|
|
321
224
|
|
|
322
225
|
console.log(pc.cyan(`\n${"=".repeat(60)}`));
|
|
323
|
-
console.log(pc.cyan(`Results: ${succeeded}
|
|
324
|
-
console.log(
|
|
226
|
+
console.log(pc.cyan(`Results: ${succeeded} specs processed`));
|
|
227
|
+
console.log(
|
|
228
|
+
pc.cyan(
|
|
229
|
+
` TypeSpec: ${compileTime.toFixed(1)}s | Python: ${pyTime.toFixed(1)}s | Total: ${totalTime.toFixed(1)}s`,
|
|
230
|
+
),
|
|
231
|
+
);
|
|
325
232
|
console.log(pc.cyan(`${"=".repeat(60)}\n`));
|
|
326
233
|
|
|
327
|
-
return
|
|
234
|
+
return pySuccess;
|
|
328
235
|
}
|
|
329
236
|
|
|
330
237
|
async function main() {
|
|
@@ -349,6 +256,8 @@ async function main() {
|
|
|
349
256
|
const startTime = performance.now();
|
|
350
257
|
let success: boolean;
|
|
351
258
|
|
|
259
|
+
await prepareBaselineOfGeneratedCode(GENERATED_FOLDER);
|
|
260
|
+
|
|
352
261
|
if (flavor) {
|
|
353
262
|
success = await regenerateFlavor(flavor, name, debug, jobs);
|
|
354
263
|
} else {
|
|
@@ -35,7 +35,7 @@ Options:
|
|
|
35
35
|
-f, --flavor <azure|unbranded> SDK flavor to test (only applies to --generator)
|
|
36
36
|
If not specified, tests both flavors
|
|
37
37
|
--env <env1,env2,...> Specific tox environments to run
|
|
38
|
-
Available: test, lint, mypy, pyright, docs, ci, unittest
|
|
38
|
+
Available: test, lint, mypy, pyright, apiview, sphinx, docs, ci, unittest
|
|
39
39
|
-j, --jobs <n> Number of parallel jobs (default: CPU cores - 2)
|
|
40
40
|
-n, --name <pattern> Filter tests by name pattern
|
|
41
41
|
-q, --quiet Suppress test output (only show pass/fail summary)
|
|
@@ -46,8 +46,10 @@ Environments (for --generator):
|
|
|
46
46
|
lint Run pylint on generated packages
|
|
47
47
|
mypy Run mypy type checking on generated packages
|
|
48
48
|
pyright Run pyright type checking on generated packages
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
apiview Run apiview validation on generated packages
|
|
50
|
+
sphinx Run sphinx docstring validation on generated packages
|
|
51
|
+
docs Run apiview + sphinx (split into parallel envs)
|
|
52
|
+
ci Run all checks (test + lint + mypy + pyright + apiview + sphinx)
|
|
51
53
|
unittest Run unit tests for pygen internals
|
|
52
54
|
|
|
53
55
|
Examples:
|
|
@@ -56,8 +58,8 @@ Examples:
|
|
|
56
58
|
run-tests.ts --generator # Run generator tests for all flavors
|
|
57
59
|
run-tests.ts --generator --flavor=azure # Run generator tests for azure only
|
|
58
60
|
run-tests.ts -g -f azure --env=test # Run pytest for azure only
|
|
59
|
-
run-tests.ts -g --env=
|
|
60
|
-
run-tests.ts -g -f unbranded --env=
|
|
61
|
+
run-tests.ts -g --env=lint # Run pylint for all flavors
|
|
62
|
+
run-tests.ts -g -f unbranded --env=mypy # Run mypy for unbranded only
|
|
61
63
|
`);
|
|
62
64
|
process.exit(0);
|
|
63
65
|
}
|
|
@@ -78,9 +80,39 @@ interface ToxResult {
|
|
|
78
80
|
success: boolean;
|
|
79
81
|
duration: number;
|
|
80
82
|
error?: string;
|
|
83
|
+
failedTests?: string[];
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
interface RunningTask {
|
|
87
|
+
env: string;
|
|
88
|
+
proc: ChildProcess;
|
|
89
|
+
promise: Promise<ToxResult>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractFailedTests(output: string): string[] {
|
|
93
|
+
const lines = output.split("\n");
|
|
94
|
+
const failedTests: string[] = [];
|
|
95
|
+
let inSummary = false;
|
|
96
|
+
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
if (line.includes("short test summary info")) {
|
|
99
|
+
inSummary = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (inSummary) {
|
|
103
|
+
const match = line.match(/^FAILED\s+(.+)/);
|
|
104
|
+
if (match) {
|
|
105
|
+
failedTests.push(match[1].trim());
|
|
106
|
+
} else if (line.match(/^={3,}/) || line.match(/^\d+ failed/)) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return failedTests;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function startToxEnv(env: string, pythonPath: string, name?: string): RunningTask {
|
|
84
116
|
const startTime = Date.now();
|
|
85
117
|
const toxIniPath = join(testsDir, "tox.ini");
|
|
86
118
|
|
|
@@ -92,20 +124,34 @@ async function runToxEnv(env: string, pythonPath: string, name?: string): Promis
|
|
|
92
124
|
|
|
93
125
|
console.log(`${pc.blue("[START]")} ${env}`);
|
|
94
126
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
});
|
|
127
|
+
const proc: ChildProcess = spawn(pythonPath, args, {
|
|
128
|
+
cwd: testsDir,
|
|
129
|
+
stdio: "pipe",
|
|
130
|
+
env: { ...process.env, FOLDER: env.split("-")[1] || "azure" },
|
|
131
|
+
});
|
|
101
132
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
133
|
+
let stderr = "";
|
|
134
|
+
let stdout = "";
|
|
135
|
+
if (proc.stdout) {
|
|
136
|
+
proc.stdout.on("data", (data) => {
|
|
137
|
+
const chunk = data.toString();
|
|
138
|
+
stdout += chunk;
|
|
139
|
+
if (!argv.values.quiet) {
|
|
140
|
+
process.stdout.write(chunk);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (proc.stderr) {
|
|
145
|
+
proc.stderr.on("data", (data) => {
|
|
146
|
+
const chunk = data.toString();
|
|
147
|
+
stderr += chunk;
|
|
148
|
+
if (!argv.values.quiet) {
|
|
149
|
+
process.stderr.write(chunk);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
108
153
|
|
|
154
|
+
const promise = new Promise<ToxResult>((resolve) => {
|
|
109
155
|
proc.on("close", (code) => {
|
|
110
156
|
const duration = (Date.now() - startTime) / 1000;
|
|
111
157
|
const success = code === 0;
|
|
@@ -116,11 +162,14 @@ async function runToxEnv(env: string, pythonPath: string, name?: string): Promis
|
|
|
116
162
|
console.log(`${pc.red("[FAIL]")} ${env} (${duration.toFixed(1)}s)`);
|
|
117
163
|
}
|
|
118
164
|
|
|
165
|
+
const failedTests = extractFailedTests(stdout);
|
|
166
|
+
|
|
119
167
|
resolve({
|
|
120
168
|
env,
|
|
121
169
|
success,
|
|
122
170
|
duration,
|
|
123
171
|
error: success ? undefined : stderr || `Exit code: ${code}`,
|
|
172
|
+
failedTests: failedTests.length > 0 ? failedTests : undefined,
|
|
124
173
|
});
|
|
125
174
|
});
|
|
126
175
|
|
|
@@ -135,6 +184,16 @@ async function runToxEnv(env: string, pythonPath: string, name?: string): Promis
|
|
|
135
184
|
});
|
|
136
185
|
});
|
|
137
186
|
});
|
|
187
|
+
|
|
188
|
+
return { env, proc, promise };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function killTask(task: RunningTask): void {
|
|
192
|
+
try {
|
|
193
|
+
task.proc.kill("SIGTERM");
|
|
194
|
+
} catch {
|
|
195
|
+
// Process may have already exited
|
|
196
|
+
}
|
|
138
197
|
}
|
|
139
198
|
|
|
140
199
|
async function runParallel(
|
|
@@ -144,24 +203,51 @@ async function runParallel(
|
|
|
144
203
|
name?: string,
|
|
145
204
|
): Promise<ToxResult[]> {
|
|
146
205
|
const results: ToxResult[] = [];
|
|
147
|
-
const running: Map<string,
|
|
206
|
+
const running: Map<string, RunningTask> = new Map();
|
|
148
207
|
|
|
149
208
|
for (const env of envs) {
|
|
150
209
|
// Wait if we're at max capacity
|
|
151
|
-
|
|
152
|
-
const
|
|
210
|
+
while (running.size >= maxJobs) {
|
|
211
|
+
const promises = Array.from(running.values()).map((t) => t.promise);
|
|
212
|
+
const completed = await Promise.race(promises);
|
|
153
213
|
results.push(completed);
|
|
154
214
|
running.delete(completed.env);
|
|
215
|
+
|
|
216
|
+
// Fail-fast: kill all running tasks and exit
|
|
217
|
+
if (!completed.success) {
|
|
218
|
+
console.log(pc.red(`\n[FAIL-FAST] ${completed.env} failed, killing remaining tasks...`));
|
|
219
|
+
for (const task of running.values()) {
|
|
220
|
+
killTask(task);
|
|
221
|
+
}
|
|
222
|
+
// Wait briefly for processes to terminate
|
|
223
|
+
await Promise.all(Array.from(running.values()).map((t) => t.promise));
|
|
224
|
+
return results;
|
|
225
|
+
}
|
|
155
226
|
}
|
|
156
227
|
|
|
157
228
|
// Start new task
|
|
158
|
-
const task =
|
|
229
|
+
const task = startToxEnv(env, pythonPath, name);
|
|
159
230
|
running.set(env, task);
|
|
160
231
|
}
|
|
161
232
|
|
|
162
|
-
// Wait for remaining tasks
|
|
163
|
-
|
|
164
|
-
|
|
233
|
+
// Wait for remaining tasks, checking for failures
|
|
234
|
+
while (running.size > 0) {
|
|
235
|
+
const promises = Array.from(running.values()).map((t) => t.promise);
|
|
236
|
+
const completed = await Promise.race(promises);
|
|
237
|
+
results.push(completed);
|
|
238
|
+
running.delete(completed.env);
|
|
239
|
+
|
|
240
|
+
// Fail-fast: kill all running tasks and exit
|
|
241
|
+
if (!completed.success) {
|
|
242
|
+
console.log(pc.red(`\n[FAIL-FAST] ${completed.env} failed, killing remaining tasks...`));
|
|
243
|
+
for (const task of running.values()) {
|
|
244
|
+
killTask(task);
|
|
245
|
+
}
|
|
246
|
+
// Wait briefly for processes to terminate
|
|
247
|
+
await Promise.all(Array.from(running.values()).map((t) => t.promise));
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
165
251
|
|
|
166
252
|
return results;
|
|
167
253
|
}
|
|
@@ -173,7 +259,8 @@ async function _runSequential(
|
|
|
173
259
|
): Promise<ToxResult[]> {
|
|
174
260
|
const results: ToxResult[] = [];
|
|
175
261
|
for (const env of envs) {
|
|
176
|
-
const
|
|
262
|
+
const task = startToxEnv(env, pythonPath, name);
|
|
263
|
+
const result = await task.promise;
|
|
177
264
|
results.push(result);
|
|
178
265
|
}
|
|
179
266
|
return results;
|
|
@@ -206,7 +293,11 @@ function printSummary(results: ToxResult[]): void {
|
|
|
206
293
|
console.log(pc.red("Failed environments:"));
|
|
207
294
|
for (const result of failed) {
|
|
208
295
|
console.log(` - ${result.env}`);
|
|
209
|
-
if (result.
|
|
296
|
+
if (result.failedTests && result.failedTests.length > 0) {
|
|
297
|
+
for (const test of result.failedTests) {
|
|
298
|
+
console.log(` ${pc.red("FAILED")} ${test}`);
|
|
299
|
+
}
|
|
300
|
+
} else if (result.error && !argv.values.quiet) {
|
|
210
301
|
console.log(` ${result.error.split("\n").slice(0, 5).join("\n ")}`);
|
|
211
302
|
}
|
|
212
303
|
}
|
|
@@ -316,12 +407,15 @@ async function main(): Promise<void> {
|
|
|
316
407
|
baseEnvs = ["test"];
|
|
317
408
|
}
|
|
318
409
|
|
|
319
|
-
// Expand 'ci' into
|
|
410
|
+
// Expand 'ci' and 'docs' into component environments for parallel execution
|
|
320
411
|
const expandedEnvs: string[] = [];
|
|
321
412
|
for (const env of baseEnvs) {
|
|
322
413
|
if (env === "ci") {
|
|
323
|
-
//
|
|
324
|
-
expandedEnvs.push("test", "lint", "mypy", "pyright", "
|
|
414
|
+
// All envs run in parallel — pre-built wheels make installs cheap
|
|
415
|
+
expandedEnvs.push("test", "lint", "mypy", "pyright", "apiview", "sphinx");
|
|
416
|
+
} else if (env === "docs") {
|
|
417
|
+
// Split docs into apiview + sphinx for parallelism
|
|
418
|
+
expandedEnvs.push("apiview", "sphinx");
|
|
325
419
|
} else {
|
|
326
420
|
expandedEnvs.push(env);
|
|
327
421
|
}
|
|
@@ -348,37 +442,48 @@ async function main(): Promise<void> {
|
|
|
348
442
|
process.exit(1);
|
|
349
443
|
}
|
|
350
444
|
|
|
351
|
-
// Separate test environments from other environments
|
|
352
|
-
// Test environments must run sequentially (they share port 3000)
|
|
353
|
-
// Other environments (lint, mypy, pyright, docs) can run in parallel
|
|
354
|
-
const testEnvs = envs.filter((e) => e.startsWith("test-") || e === "unittest");
|
|
355
|
-
const otherEnvs = envs.filter((e) => !e.startsWith("test-") && e !== "unittest");
|
|
356
|
-
|
|
357
445
|
const maxJobs = argv.values.jobs
|
|
358
446
|
? parseInt(argv.values.jobs, 10)
|
|
359
447
|
: Math.max(2, cpus().length - 2);
|
|
360
448
|
|
|
361
449
|
console.log(` Flavors: ${flavors.join(", ")}`);
|
|
362
450
|
console.log(` Environments: ${envs.join(", ")}`);
|
|
363
|
-
console.log(` Jobs: ${maxJobs}
|
|
451
|
+
console.log(` Jobs: ${maxJobs}`);
|
|
364
452
|
if (argv.values.name) {
|
|
365
453
|
console.log(` Filter: ${argv.values.name}`);
|
|
366
454
|
}
|
|
367
455
|
console.log();
|
|
368
456
|
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
457
|
+
// Pre-build wheels for each flavor so tox envs install from pre-built
|
|
458
|
+
// wheels instead of rebuilding from source (~2min build once vs ~2min × N envs)
|
|
459
|
+
console.log(pc.cyan("Pre-building wheels for all flavors..."));
|
|
460
|
+
const installScript = join(testsDir, "install_packages.py");
|
|
461
|
+
for (const flavor of flavors) {
|
|
462
|
+
const startTime = Date.now();
|
|
463
|
+
const proc = spawn(pythonPath, [installScript, "build", flavor, testsDir], {
|
|
464
|
+
cwd: testsDir,
|
|
465
|
+
stdio: "inherit",
|
|
466
|
+
});
|
|
467
|
+
await new Promise<void>((resolve) => {
|
|
468
|
+
proc.on("close", (code) => {
|
|
469
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
470
|
+
if (code === 0) {
|
|
471
|
+
console.log(`${pc.green("[PASS]")} wheel build ${flavor} (${duration}s)`);
|
|
472
|
+
} else {
|
|
473
|
+
console.log(
|
|
474
|
+
`${pc.yellow("[WARN]")} wheel build ${flavor} failed (${duration}s), tox envs will build from source`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
resolve();
|
|
478
|
+
});
|
|
479
|
+
});
|
|
374
480
|
}
|
|
481
|
+
console.log();
|
|
375
482
|
|
|
376
|
-
// Run
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
results = results.concat(otherResults);
|
|
381
|
-
}
|
|
483
|
+
// Run all environments in parallel
|
|
484
|
+
// The mock server serves both azure and unbranded specs, so all tests can run together
|
|
485
|
+
console.log(pc.cyan("Running all environments in parallel..."));
|
|
486
|
+
const results = await runParallel(envs, pythonPath, maxJobs, argv.values.name);
|
|
382
487
|
|
|
383
488
|
allResults.push(...results);
|
|
384
489
|
}
|