Skip to content

Spector test for server csharp #6609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3e52206
initial real server impl
allenjzhang Mar 17, 2025
582f624
Merge branch 'main' of https://github.com/microsoft/typespec into azh…
allenjzhang Mar 19, 2025
2c04434
initial checkin
allenjzhang Mar 23, 2025
87e3ce8
pre-merge
allenjzhang Mar 23, 2025
497ace5
Merge branch 'main' of https://github.com/microsoft/typespec into azh…
allenjzhang Mar 23, 2025
aeae1cd
emit and build working
allenjzhang Mar 23, 2025
0e962b4
adding spector tests
allenjzhang Mar 24, 2025
d8292a1
adding individual port numbers and spector test base
allenjzhang Mar 24, 2025
223ce76
refactor: update server logging and improve test structure for http-s…
allenjzhang Mar 25, 2025
73e6861
completed implementation
allenjzhang Mar 25, 2025
fb5c19e
revert spector code change
allenjzhang Mar 25, 2025
7ad73d6
revert pnpm-lock.yaml
allenjzhang Mar 25, 2025
9aaa09b
adding changelog
allenjzhang Mar 25, 2025
bf5e155
PR comments
allenjzhang Mar 25, 2025
b81630c
build fix
allenjzhang Mar 25, 2025
a53260e
Merge branch 'main' of https://github.com/microsoft/typespec into azh…
allenjzhang Mar 25, 2025
41818ea
fixing test check failure
allenjzhang Mar 25, 2025
2c61d65
change to `run` from `execa` and cleaned up package dependency
allenjzhang Mar 25, 2025
7d27541
Merge branch 'main' into azhang_ServerTest
allenjzhang Mar 25, 2025
0680802
Merge branch 'main' into azhang_ServerTest
allenjzhang Mar 25, 2025
07b985c
Merge branch 'main' into azhang_ServerTest
allenjzhang Mar 27, 2025
322fe1d
update .testignore
allenjzhang Mar 27, 2025
be32e0e
updated with latest path option
allenjzhang Mar 27, 2025
940b034
updated emit script to TS
allenjzhang Mar 27, 2025
485650b
Merge branch 'main' into azhang_ServerTest
allenjzhang Mar 27, 2025
349e183
update lock file.
allenjzhang Mar 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/azhang_ServerTest-2025-2-24-23-28-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/http-server-csharp"
---

Adding http-spec scenarios test infrastructure
2 changes: 2 additions & 0 deletions packages/http-server-csharp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test/scenarios/generated/
temp/
21 changes: 6 additions & 15 deletions packages/http-server-csharp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

- [#6507](https://github.com/microsoft/typespec/pull/6507) Scaffolding updates for rc


## 0.58.0-alpha.11

### Breaking Changes
Expand All @@ -27,15 +26,13 @@
- [#6443](https://github.com/microsoft/typespec/pull/6443) Fix handling of record types
- [#6411](https://github.com/microsoft/typespec/pull/6411) Add support for new `dryRun` emitter option


## 0.58.0-alpha.10

### Bug Fixes

- [#6177](https://github.com/microsoft/typespec/pull/6177) Fix issues with sample mock tests
- [#5952](https://github.com/microsoft/typespec/pull/5952) Fixes to enums, operation signatures


## 0.58.0-alpha.9

No changes, version bump only.
Expand All @@ -51,48 +48,42 @@ No changes, version bump only.

- [#5690](https://github.com/microsoft/typespec/pull/5690) Upgrade dependencies


## 0.58.0-alpha.7

### Bug Fixes

- [#5505](https://github.com/microsoft/typespec/pull/5505) [http-server-csharp]: Fix routing issues with MFD requests
- [#5417](https://github.com/microsoft/typespec/pull/5417) Handle multipart operations in c-sharp service emitter


## 0.58.0-alpha.6

### Bug Fixes

- [#5140](https://github.com/microsoft/typespec/pull/5140) Fix #4308 Process sub-namespace of a service in csharp service emitter
Fix #4998 Generator throws on void return type
Fix #5000 Tuple types are not properly generated
Fix #5001 OkResponse is generated as a model
Fix #5024 Literal type is not properly generated
Fix #5124 Templated model reported error while generating
Fix #5125 No interfaces and controllers are generated for ops in a namespace
Fix #4998 Generator throws on void return type
Fix #5000 Tuple types are not properly generated
Fix #5001 OkResponse is generated as a model
Fix #5024 Literal type is not properly generated
Fix #5124 Templated model reported error while generating
Fix #5125 No interfaces and controllers are generated for ops in a namespace
- [#5279](https://github.com/microsoft/typespec/pull/5279) Fix nullable types, anonymous types, and safeInt


## 0.58.0-alpha.5

### Bump dependencies

- [#4679](https://github.com/microsoft/typespec/pull/4679) Upgrade dependencies - October 2024


## 0.58.0-alpha.4

### Bump dependencies

- [#4424](https://github.com/microsoft/typespec/pull/4424) Bump dependencies


## 0.58.0-alpha.3

No changes, version bump only.

## 0.58.0-alpha.2

No changes, version bump only.

292 changes: 292 additions & 0 deletions packages/http-server-csharp/eng/scripts/emit-scenarios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
/* eslint-disable no-console */
import { run } from "@typespec/internal-build-utils";
import { copy, pathExists } from "fs-extra";
import { mkdir, readFile, rm, writeFile } from "fs/promises";
import { globby } from "globby";
import inquirer from "inquirer";
import ora from "ora";
import pLimit from "p-limit";
import { basename, dirname, join, resolve } from "pathe";
import pc from "picocolors";
import { fileURLToPath } from "url";
import { parseArgs } from "util";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const tspConfig = join(__dirname, "tspconfig.yaml");

const basePath = join(__dirname, "../..");
const logDirRoot = join(basePath, "temp", "emit-scenarios-logs");
const reportFilePath = join(logDirRoot, "report.txt");
const testScenarioPath = join(basePath, "test", "scenarios");
const ignoreFilePath = join(testScenarioPath, ".testignore");

// Use `parseArgs` to parse command-line arguments
const argv = parseArgs({
args: process.argv.slice(2),
options: {
input: { type: "string", default: join(basePath, "..", "http-specs", "specs") },
build: { type: "boolean", default: true },
interactive: { type: "boolean", default: false },
},
strict: false, // Allow unknown arguments
});

const specDir = resolve(argv.values.input);

// Initialize the port number
let portNumber = 65000;

// Remove the log directory if it exists.
async function clearDirectory(dirRoot) {
if (await pathExists(dirRoot)) {
await rm(dirRoot, { recursive: true, force: true });
}
}

async function copySelectiveFiles(extension, sourceDir, targetDir) {
const files = await globby(extension, { cwd: sourceDir });
for (const file of files) {
const src = join(sourceDir, file);
const dest = join(targetDir, file);
await copy(src, dest);
}
}

async function compileSpec(file, options) {
const relativePath = file;
const fullPath = resolve(specDir, relativePath);
const { build, interactive } = options;
const patchFileDir = join(testScenarioPath, dirname(relativePath));
const outputDir = join(testScenarioPath, "generated", dirname(relativePath));
const logDir = join(logDirRoot, dirname(relativePath));

let spinner;
if (interactive) {
spinner = ora({ text: `Processing: ${relativePath}`, color: "cyan" }).start();
}

try {
if (await pathExists(outputDir)) {
if (spinner) spinner.text = `Clearing directory: ${outputDir}`;
await rm(outputDir, { recursive: true, force: true });
}
if (spinner) spinner.text = `Creating directory: ${outputDir}`;
await mkdir(outputDir, { recursive: true });

// Increment the port number for each folder
portNumber++;

// Compile the spec and generate server code
if (spinner) spinner.text = `Generating csharp server code: ${relativePath}`;
await run(
"npx",
[
"tsp",
"compile",
fullPath,
"--emit",
resolve(import.meta.dirname, "../.."),
"--config",
tspConfig,
"--output-dir",
join(outputDir, "generated"),
"--arg",
`service-port-http=${portNumber.toString()}`,
"--arg",
`service-port-https=${(portNumber + 500).toString()}`,
],
{
stdio: "ignore",
silent: true,
},
);

if (spinner) spinner.text = `Formatting with dotnet: ${relativePath}`;
await run(
"dotnet",
["format"],
{ cwd: outputDir },
{
stdio: "ignore",
silent: true,
},
);

if (build) {
if (spinner) spinner.text = `Building project: ${relativePath}`;
await run(
"dotnet",
["build"],
{ cwd: outputDir },
{
stdio: "ignore",
silent: true,
},
);
}

if (await pathExists(patchFileDir)) {
const mockDir = join(outputDir, "mocks");
if (spinner) spinner.text = `Copying mock patch files to: ${mockDir}`;
await copySelectiveFiles("*.cs", patchFileDir, mockDir);
}

if (spinner) {
spinner.succeed(`Finished processing: ${relativePath}`);
}
return { status: "succeeded", relativePath, portNumber };
} catch (error) {
if (spinner) {
spinner.fail(`Failed processing: ${relativePath}`);
}
const errorDetails = error.stdout || error.stderr || error.message;

// Write error details to a log file.
await mkdir(logDir, { recursive: true });
const logFilePath = join(logDir, `${basename(relativePath, ".tsp")}-error.log`);
await writeFile(logFilePath, errorDetails, "utf8");

if (interactive) {
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: `Processing failed for ${relativePath}. What would you like to do?`,
choices: [
{ name: "Retry", value: "retry" },
{ name: "Skip to next file", value: "next" },
{ name: "Abort processing", value: "abort" },
],
},
]);

if (action === "retry") {
if (spinner) spinner.start(`Retrying: ${relativePath}`);
return await compileSpec(file, options);
} else if (action === "next") {
console.log(pc.yellow(`Skipping: ${relativePath}`));
} else if (action === "abort") {
console.log(pc.red("Aborting processing."));
throw new Error("Processing aborted by user");
}
}
return { status: "failed", relativePath, errorDetails };
}
}

async function processFiles(files, options) {
const { interactive } = options;
const succeeded = [];
const failed = [];

if (interactive) {
// Sequential processing so each spinner is visible.
for (const file of files) {
try {
const result = await processFile(file, options);
if (result.status === "succeeded") {
succeeded.push(result.relativePath);
} else {
failed.push({ relativePath: result.relativePath, errorDetails: result.errorDetails });
}
} catch (err) {
break;
}
}
} else {
// Global progress spinner.
const total = files.length;
let completed = 0;
const globalSpinner = ora({ text: `Processing 0/${total} files...`, color: "cyan" }).start();
const limit = pLimit(4);
const tasks = files.map((file) =>
limit(() =>
compileSpec(file, options).then((result) => {
completed++;
globalSpinner.text = `Processing ${completed}/${total} files...`;
return result;
}),
),
);
const results = await Promise.all(tasks);
globalSpinner.succeed(`Processed ${total} files`);
for (const result of results) {
if (result.status === "succeeded") {
succeeded.push(result.relativePath);
} else {
failed.push({ relativePath: result.relativePath, errorDetails: result.errorDetails });
}
}
}

console.log(pc.bold(pc.green("\nProcessing Complete:")));
console.log(pc.green(`Succeeded: ${succeeded.length}`));
console.log(pc.red(`Failed: ${failed.length}`));

if (failed.length > 0) {
console.log(pc.red("\nFailed Specs:"));
failed.forEach((f) => {
console.log(pc.red(` - ${f.relativePath}`));
});
console.log(pc.blue(`\nLogs available at: ${logDirRoot}`));
}

// Ensure the log directory exists before writing the report.
await mkdir(logDirRoot, { recursive: true });
const report = [
"Succeeded Files:",
...succeeded.map((f) => ` - ${f}`),
"Failed Files:",
...failed.map((f) => ` - ${f.relativePath}\n Error: ${f.errorDetails}`),
].join("\n");
await writeFile(reportFilePath, report, "utf8");
console.log(pc.blue(`Report written to: ${reportFilePath}`));
}

// Read and parse the ignore file.
async function getIgnoreList() {
try {
const content = await readFile(ignoreFilePath, "utf8");
return content
.split(/\r?\n/)
.filter((line) => line.trim() && !line.startsWith("#"))
.map((line) => line.trim());
} catch {
console.warn(pc.yellow("No ignore file found."));
return [];
}
}

async function main() {
const startTime = process.hrtime.bigint();
let exitCode = 0;
try {
await clearDirectory(logDirRoot); // ✅ Clear logs at the start
await clearDirectory(join(testScenarioPath, "generated")); // ✅ Clear output folders at the start

const ignoreList = await getIgnoreList();

const patterns = ["**/main.tsp"];
const specsList = await globby(patterns, { cwd: specDir });

const paths = specsList.filter((item) => !ignoreList.includes(item));

await processFiles(paths, {
interactive: argv.values.interactive,
build: argv.values.build,
});
} catch (error) {
console.error(pc.red(`❌ Fatal Error: ${error.message}`));
exitCode = 1; // ✅ Ensure graceful failure handling
} finally {
// ✅ Always log execution time before exit
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1e9; // Convert nanoseconds to seconds
console.log(pc.blue(`⏱️ Total execution time: ${duration.toFixed(2)} seconds`));

process.exit(exitCode); // ✅ Ensures proper exit handling
}
}

main();
Loading
Loading