diff --git a/test/ConsoleApp/Directory.Build.props b/test/ConsoleApp/Directory.Build.props index 06de0c508..cd0f563cc 100644 --- a/test/ConsoleApp/Directory.Build.props +++ b/test/ConsoleApp/Directory.Build.props @@ -5,6 +5,12 @@ true true + + + true + false + Program.cs diff --git a/test/multiple-sources.refitter b/test/multiple-sources.refitter index 82e85c206..d013fc23d 100644 --- a/test/multiple-sources.refitter +++ b/test/multiple-sources.refitter @@ -4,5 +4,7 @@ "OpenAPI/v3.0/petstore.json", "OpenAPI/v3.0/api-with-examples.json" ], + "namespace": "MultipleSources", + "outputFolder": "GeneratedCode", "multipleInterfaces": "bytag" } diff --git a/test/smoke-tests.bat b/test/smoke-tests.bat index c65009102..6b8e26375 100644 --- a/test/smoke-tests.bat +++ b/test/smoke-tests.bat @@ -2,14 +2,20 @@ setlocal enabledelayedexpansion :: Parse command line arguments -set "PARALLEL=true" +set "THROTTLE_LIMIT=4" set "USE_PRODUCTION=false" set "USE_DOCKER=false" :parse_args if "%~1"=="" goto :main +if /i "%~1"=="-throttlelimit" ( + set "THROTTLE_LIMIT=%~2" + shift + shift + goto :parse_args +) if /i "%~1"=="-parallel" ( - set "PARALLEL=%~2" + :: Kept for backward compatibility shift shift goto :parse_args @@ -28,11 +34,11 @@ shift goto :parse_args :main -echo Parallel: %PARALLEL% +echo ThrottleLimit: %THROTTLE_LIMIT% echo UseProduction: %USE_PRODUCTION% echo UseDocker: %USE_DOCKER% -goto :run_tests +goto :start_tests :throw_on_native_failure if not %errorlevel%==0 ( @@ -41,87 +47,81 @@ if not %errorlevel%==0 ( ) goto :eof -:generate_and_build -set "format=%~1" -set "namespace=%~2" -set "output_path=%~3" -set "args=%~4" -set "net_core=%~5" -set "csproj=%~6" -set "build_from_source=%~7" -set "use_docker=%~8" - -if "%build_from_source%"=="" set "build_from_source=true" -if "%use_docker%"=="" set "use_docker=false" - -:: Clean up generated files -del /q /s ".\GeneratedCode\*.cs" 2>nul - -set "process_path=.\bin\refitter" -if /i "%build_from_source%"=="false" set "process_path=refitter" -if /i "%use_docker%"=="true" set "process_path=docker" +:generate +:: %1=specPath %2=namespace %3=outputPath %4=extraArgs %5=processPath %6=useDocker +set "spec_path=%~1" +set "gen_namespace=%~2" +set "gen_output=%~3" +set "gen_args=%~4" +set "gen_process=%~5" +set "gen_docker=%~6" -:: Check if using docker -if /i "%use_docker%"=="true" ( +if /i "%gen_docker%"=="true" ( set "current_dir=%CD%" set "current_dir=!current_dir:\=/!" - - :: Check if using settings file - echo %args% | findstr "settings-file" >nul - if %errorlevel%==0 ( - echo docker run --rm -v "!current_dir!:/src" -w /src christianhelle/refitter --no-logging %args% - docker run --rm -v "!current_dir!:/src" -w /src christianhelle/refitter --no-logging %args% - call :throw_on_native_failure - ) else ( - echo docker run --rm -v "!current_dir!:/src" -w /src christianhelle/refitter .\openapi.%format% --namespace %namespace% --output .\GeneratedCode\%output_path% --no-logging %args% - docker run --rm -v "!current_dir!:/src" -w /src christianhelle/refitter .\openapi.%format% --namespace %namespace% --output .\GeneratedCode\%output_path% --no-logging %args% - call :throw_on_native_failure - ) - goto :build_project -) - -:: Check if using settings file -echo %args% | findstr "settings-file" >nul -if %errorlevel%==0 ( - echo %process_path% --no-logging %args% - %process_path% --no-logging %args% + echo docker run --rm -v "!current_dir!:/src" -w /src christianhelle/refitter !spec_path! --namespace !gen_namespace! --output !gen_output! --no-logging !gen_args! + docker run --rm -v "!current_dir!:/src" -w /src christianhelle/refitter !spec_path! --namespace !gen_namespace! --output !gen_output! --no-logging !gen_args! call :throw_on_native_failure ) else ( - echo %process_path% .\openapi.%format% --namespace %namespace% --output .\GeneratedCode\%output_path% --no-logging %args% - %process_path% .\openapi.%format% --namespace %namespace% --output .\GeneratedCode\%output_path% --no-logging %args% + echo !gen_process! !spec_path! --namespace !gen_namespace! --output !gen_output! --no-logging !gen_args! + !gen_process! !spec_path! --namespace !gen_namespace! --output !gen_output! --no-logging !gen_args! call :throw_on_native_failure ) +goto :eof -:build_project +:generate_from_settings +:: %1=settingsFile %2=processPath %3=useDocker +set "settings_file=%~1" +set "gfs_process=%~2" +set "gfs_docker=%~3" -:: Build the project -if not "%csproj%"=="" ( - echo. - echo Building %csproj% file - echo. - set "solution=%csproj%" +if /i "%gfs_docker%"=="true" ( + set "current_dir=%CD%" + set "current_dir=!current_dir:\=/!" + echo docker run --rm -v "!current_dir!:/src" -w /src christianhelle/refitter --no-logging --settings-file !settings_file! + docker run --rm -v "!current_dir!:/src" -w /src christianhelle/refitter --no-logging --settings-file !settings_file! + call :throw_on_native_failure ) else ( - echo. - echo Building ConsoleApp - echo. - set "solution=.\ConsoleApp\ConsoleApp.slnx" - if /i "%net_core%"=="true" set "solution=.\ConsoleApp\ConsoleApp.Core.slnx" + echo !gfs_process! --no-logging --settings-file !settings_file! + !gfs_process! --no-logging --settings-file !settings_file! + call :throw_on_native_failure ) +goto :eof + +:build_solution +:: %1=solution %2=noRestore (true/false) %3=smokeTest (true/false) +set "build_sln=%~1" +set "build_norestore=%~2" +set "build_smoketest=%~3" + +set "build_extra=" +if /i "%build_norestore%"=="true" set "build_extra=!build_extra! --no-restore" +if /i "%build_smoketest%"=="true" set "build_extra=!build_extra! --property:SmokeTest=true" -dotnet build %solution% +echo. +echo Building %build_sln% +echo. +dotnet build %build_sln% --nologo -v q --property WarningLevel=0 /clp:ErrorsOnly!build_extra! call :throw_on_native_failure goto :eof +:clean_generated_code +del /q /s ".\GeneratedCode\*.cs" 2>nul +for /d %%d in (".\GeneratedCode\*") do rd /s /q "%%d" 2>nul +goto :eof + :run_tests -set "method=%~1" -set "parallel_param=%~2" -set "build_from_source_param=%~3" -set "use_docker_param=%~4" +set "build_from_source=%~1" +set "use_docker_param=%~2" -if "%build_from_source_param%"=="" set "build_from_source_param=true" +if "%build_from_source%"=="" set "build_from_source=true" if "%use_docker_param%"=="" set "use_docker_param=false" -:: Array of filenames (simulated using variables) +set "process_path=.\bin\refitter" +if /i "%build_from_source%"=="false" set "process_path=refitter" +if /i "%use_docker_param%"=="true" set "process_path=docker" + +:: Array of filenames set "filename0=weather" set "filename1=bot.paths" set "filename2=petstore" @@ -138,10 +138,17 @@ set "filename12=hubspot-events" set "filename13=hubspot-webhooks" set "filename_count=14" -if /i "%build_from_source_param%"=="true" ( +:: V3.1 filenames +set "v31filename0=webhook-example" +set "v31filename_count=1" + +:: ========================================== +:: Phase 0: Build refitter from source +:: ========================================== +if /i "%build_from_source%"=="true" ( if /i "%use_docker_param%"=="false" ( - echo dotnet publish ..\src\Refitter\Refitter.csproj -p:PublishReadyToRun=true -o bin -f net8.0 - dotnet publish ..\src\Refitter\Refitter.csproj -p:PublishReadyToRun=true -o bin -f net8.0 + echo dotnet publish ..\src\Refitter\Refitter.csproj -c Release -o bin -f net9.0 + dotnet publish ..\src\Refitter\Refitter.csproj -c Release -o bin -f net9.0 call :throw_on_native_failure echo refitter --version @@ -150,62 +157,234 @@ if /i "%build_from_source_param%"=="true" ( ) ) -:: Generate with settings files -call :generate_and_build " " " " "SwaggerPetstoreDirect.generated.cs" "--settings-file .\petstore.refitter" "" "" "%build_from_source_param%" "%use_docker_param%" -call :generate_and_build " " " " "" "--settings-file .\Apizr\petstore.apizr.refitter" "" ".\Apizr\Sample.csproj" "%build_from_source_param%" "%use_docker_param%" -call :generate_and_build " " " " "" "--settings-file .\MultipleFiles\petstore.refitter" "" "MultipleFiles\Client\Client.csproj" "%build_from_source_param%" "%use_docker_param%" +:: ========================================== +:: Phase 1: Pre-restore packages +:: ========================================== +echo. +echo === Pre-restoring packages === +echo. +dotnet restore .\ConsoleApp\ConsoleApp.slnx --nologo -v q +dotnet restore .\ConsoleApp\ConsoleApp.Core.slnx --nologo -v q +dotnet restore .\Apizr\Sample.csproj --nologo -v q + +:: ========================================== +:: Phase 2: Settings-file tests +:: ========================================== +echo. +echo === Settings-file tests === +echo. + +call :clean_generated_code +call :generate_from_settings ".\petstore.refitter" "%process_path%" "%use_docker_param%" +call :build_solution ".\ConsoleApp\ConsoleApp.slnx" "true" + +call :clean_generated_code +call :generate_from_settings ".\Apizr\petstore.apizr.refitter" "%process_path%" "%use_docker_param%" +call :build_solution ".\Apizr\Sample.csproj" "true" + +call :generate_from_settings ".\MultipleFiles\petstore.refitter" "%process_path%" "%use_docker_param%" +call :build_solution "MultipleFiles\Client\Client.csproj" "false" + +call :clean_generated_code +call :generate_from_settings ".\multiple-sources.refitter" "%process_path%" "%use_docker_param%" +call :build_solution ".\ConsoleApp\ConsoleApp.Core.slnx" "true" + +:: ========================================== +:: Phase 3: Generate all STANDARD variants +:: ========================================== +echo. +echo === Generating standard variants === +echo. +call :clean_generated_code + +:: Standard variant definitions (suffix, prefix, args) +:: We process versions/formats/filenames and generate ALL variants, then build once + +for %%v in (v3.0 v2.0) do ( + set "version_tag=%%v" + set "version_tag=!version_tag:.=!" + for %%f in (json yaml) do ( + for /l %%i in (0,1,13) do ( + set "cur_filename=!filename%%i!" + set "file_path=.\OpenAPI\%%v\!cur_filename!.%%f" + + if exist "!file_path!" ( + :: Create output base and namespace + set "output_base=!cur_filename!" + set "output_base=!output_base:-=!" + set "output_base=!output_base:.=!" + call :capitalize output_base + set "ns=!output_base!_!version_tag!_%%f" + set "file_tag=!version_tag!_%%f_!output_base!" + + :: Standard variants + call :generate "!file_path!" "!ns!.Cancellation" ".\GeneratedCode\WithCancellation!file_tag!.generated.cs" "--cancellation-tokens" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.Internal" ".\GeneratedCode\Internal!file_tag!.generated.cs" "--internal" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.UsingApiResponse" ".\GeneratedCode\IApi!file_tag!.generated.cs" "--use-api-response" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.UsingIObservable" ".\GeneratedCode\IObservable!file_tag!.generated.cs" "--use-observable-response" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.UsingIsoDateFormat" ".\GeneratedCode\UsingIsoDateFormat!file_tag!.generated.cs" "--use-iso-date-format" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.MultipleInterfaces" ".\GeneratedCode\MultipleInterfaces!file_tag!.generated.cs" "--multiple-interfaces ByEndpoint" "%process_path%" "%use_docker_param%" + :: NOTE: --multiple-interfaces ByEndpoint --operation-name-template generates duplicate types (known limitation) + :: Tested as generate-only after the batch build + call :generate "!file_path!" "!ns!.ContractOnly" ".\GeneratedCode\ContractOnly!file_tag!.generated.cs" "--contract-only" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.DynamicQuerystring" ".\GeneratedCode\DynamicQuerystring!file_tag!.generated.cs" "--use-dynamic-querystring-parameters" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.IntegerTypeInt64" ".\GeneratedCode\IntegerTypeInt64!file_tag!.generated.cs" "--integer-type Int64" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.TrimUnusedSchema" ".\GeneratedCode\TrimUnusedSchema!file_tag!.generated.cs" "--trim-unused-schema" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.OptionalNullable" ".\GeneratedCode\OptionalNullable!file_tag!.generated.cs" "--optional-nullable-parameters" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.NoDeprecated" ".\GeneratedCode\NoDeprecated!file_tag!.generated.cs" "--no-deprecated-operations" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.NoAutoGeneratedHeader" ".\GeneratedCode\NoAutoGenHeader!file_tag!.generated.cs" "--no-auto-generated-header" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.NoAcceptHeaders" ".\GeneratedCode\NoAcceptHeaders!file_tag!.generated.cs" "--no-accept-headers" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.SkipDefaultAdditionalProps" ".\GeneratedCode\SkipDefaultAddlProps!file_tag!.generated.cs" "--skip-default-additional-properties" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.NoInlineJsonConverters" ".\GeneratedCode\NoInlineJsonConv!file_tag!.generated.cs" "--no-inline-json-converters" "%process_path%" "%use_docker_param%" + :: Petstore-only variants (tag/path filters require petstore-specific tags) + echo !cur_filename! | findstr /b "petstore" >nul && ( + call :generate "!file_path!" "!ns!.TagFiltered" ".\GeneratedCode\TagFiltered!file_tag!.generated.cs" "--tag pet --tag user --tag store" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.MatchPathFiltered" ".\GeneratedCode\MatchPathFiltered!file_tag!.generated.cs" "--match-path ^^/pet/.*" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.MultipleInterfacesByTag" ".\GeneratedCode\MultipleInterfacesByTag!file_tag!.generated.cs" "--multiple-interfaces ByTag" "%process_path%" "%use_docker_param%" + ) + :: Multiple files variant (unique subdirectory) + call :generate "!file_path!" "!ns!.MultipleFiles" ".\GeneratedCode\MultipleFiles\!file_tag!\" "--multiple-files" "%process_path%" "%use_docker_param%" + :: Separate contracts variant (unique subdirectories) + call :generate "!file_path!" "!ns!.SeparateContractsFile" ".\GeneratedCode\SeparateContracts\!file_tag!\" "--contracts-output GeneratedCode\Contracts\!file_tag! --contracts-namespace !ns!.SeparateContractsFile.Contracts" "%process_path%" "%use_docker_param%" + ) + ) + ) +) + +:: V3.1 specs (skip MultipleInterfaces variants - webhook specs may lack regular API paths) +for %%f in (json yaml) do ( + for /l %%i in (0,1,0) do ( + set "cur_filename=!v31filename%%i!" + set "file_path=.\OpenAPI\v3.1\!cur_filename!.%%f" + + if exist "!file_path!" ( + set "output_base=!cur_filename!" + set "output_base=!output_base:-=!" + set "output_base=!output_base:.=!" + call :capitalize output_base + set "ns=!output_base!_v31_%%f" + set "file_tag=v31_%%f_!output_base!" + + call :generate "!file_path!" "!ns!.Cancellation" ".\GeneratedCode\WithCancellation!file_tag!.generated.cs" "--cancellation-tokens" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.Internal" ".\GeneratedCode\Internal!file_tag!.generated.cs" "--internal" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.UsingApiResponse" ".\GeneratedCode\IApi!file_tag!.generated.cs" "--use-api-response" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.UsingIObservable" ".\GeneratedCode\IObservable!file_tag!.generated.cs" "--use-observable-response" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.UsingIsoDateFormat" ".\GeneratedCode\UsingIsoDateFormat!file_tag!.generated.cs" "--use-iso-date-format" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.ContractOnly" ".\GeneratedCode\ContractOnly!file_tag!.generated.cs" "--contract-only" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.DynamicQuerystring" ".\GeneratedCode\DynamicQuerystring!file_tag!.generated.cs" "--use-dynamic-querystring-parameters" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.IntegerTypeInt64" ".\GeneratedCode\IntegerTypeInt64!file_tag!.generated.cs" "--integer-type Int64" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.TrimUnusedSchema" ".\GeneratedCode\TrimUnusedSchema!file_tag!.generated.cs" "--trim-unused-schema" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.OptionalNullable" ".\GeneratedCode\OptionalNullable!file_tag!.generated.cs" "--optional-nullable-parameters" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.NoDeprecated" ".\GeneratedCode\NoDeprecated!file_tag!.generated.cs" "--no-deprecated-operations" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.NoAutoGeneratedHeader" ".\GeneratedCode\NoAutoGenHeader!file_tag!.generated.cs" "--no-auto-generated-header" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.NoAcceptHeaders" ".\GeneratedCode\NoAcceptHeaders!file_tag!.generated.cs" "--no-accept-headers" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.SkipDefaultAdditionalProps" ".\GeneratedCode\SkipDefaultAddlProps!file_tag!.generated.cs" "--skip-default-additional-properties" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.NoInlineJsonConverters" ".\GeneratedCode\NoInlineJsonConv!file_tag!.generated.cs" "--no-inline-json-converters" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.MultipleFiles" ".\GeneratedCode\MultipleFiles\!file_tag!\" "--multiple-files" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.SeparateContractsFile" ".\GeneratedCode\SeparateContracts\!file_tag!\" "--contracts-output GeneratedCode\Contracts\!file_tag! --contracts-namespace !ns!.SeparateContractsFile.Contracts" "%process_path%" "%use_docker_param%" + ) + ) +) + +:: ========================================== +:: Phase 4: Build standard variants (one build validates all) +:: ========================================== +echo. +echo === Building standard variants === +echo. +call :build_solution ".\ConsoleApp\ConsoleApp.slnx" "true" "true" + +:: ========================================== +:: Phase 4b: Generate-only test for MultipleInterfacesWithCustomName +:: This variant uses --multiple-interfaces ByEndpoint --operation-name-template which +:: generates duplicate types per-endpoint (known limitation). We verify generation succeeds. +:: ========================================== +echo. +echo === Generate-only: MultipleInterfacesWithCustomName (petstore) === +echo. +call :generate ".\OpenAPI\v3.0\petstore.json" "GenerateOnly.MultipleInterfacesWithCustomName" ".\GeneratedCode\MultipleInterfacesWithCustomName_generateonly.cs" "--multiple-interfaces ByEndpoint --operation-name-template ExecuteAsync" "%process_path%" "%use_docker_param%" +if not exist ".\GeneratedCode\MultipleInterfacesWithCustomName_generateonly.cs" ( + echo Generate-only test failed: MultipleInterfacesWithCustomName + exit /b 1 +) +del /q ".\GeneratedCode\MultipleInterfacesWithCustomName_generateonly.cs" +echo Generate-only test passed: MultipleInterfacesWithCustomName + +:: ========================================== +:: Phase 5: Generate netCore variants (accumulate on top of standard code) +:: ========================================== +echo. +echo === Generating netCore variants === +echo. -:: Process versions and formats for %%v in (v3.0 v2.0) do ( + set "version_tag=%%v" + set "version_tag=!version_tag:.=!" for %%f in (json yaml) do ( for /l %%i in (0,1,13) do ( - set "filename=!filename%%i!" - set "file_path=.\OpenAPI\%%v\!filename!.%%f" + set "cur_filename=!filename%%i!" + set "file_path=.\OpenAPI\%%v\!cur_filename!.%%f" if exist "!file_path!" ( - copy "!file_path!" ".\openapi.%%f" >nul - - :: Create output path and namespace - set "output_path=!filename!.generated.cs" - set "output_path=!output_path:~0,1!!output_path:~1!" - call :capitalize output_path - - set "namespace=!filename!" - set "namespace=!namespace:-=!" - call :capitalize namespace - - :: Generate different variants - call :generate_and_build "%%f" "!namespace!.Disposable" "Disposable!output_path!" "--disposable" "true" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.MultipleFiles" "" "--multiple-files" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.SeparateContractsFile" "" "--contracts-output GeneratedCode/Contracts --contracts-namespace !namespace!.SeparateContractsFile.Contracts" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.Cancellation" "WithCancellation!output_path!" "--cancellation-tokens" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.Internal" "Internal!output_path!" "--internal" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.UsingApiResponse" "IApi!output_path!" "--use-api-response" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.UsingIObservable" "IObservable!output_path!" "--use-observable-response" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.UsingIsoDateFormat" "UsingIsoDateFormat!output_path!" "--use-iso-date-format" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.MultipleInterfaces" "MultipleInterfaces!output_path!" "--multiple-interfaces ByEndpoint" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.MultipleInterfaces" "MultipleInterfacesWithCustomName!output_path!" "--multiple-interfaces ByEndpoint --operation-name-template ExecuteAsync" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.TagFiltered" "TagFiltered!output_path!" "--tag pet --tag user --tag store" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.MatchPathFiltered" "MatchPathFiltered!output_path!" "--match-path ^^/pet/.*" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.ContractOnly" "ContractOnly!output_path!" "--contract-only" "" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.ImmutableRecords" "ImmutableRecords!output_path!" "--immutable-records" "true" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.PolymorphicSerialization" "PolymorphicSerialization!output_path!" "--use-polymorphic-serialization" "true" "" "%build_from_source_param%" "%use_docker_param%" - call :generate_and_build "%%f" "!namespace!.CollectionFormatCsv" "CollectionFormatCsv!output_path!" "--collection-format csv" "true" "" "%build_from_source_param%" "%use_docker_param%" + set "output_base=!cur_filename!" + set "output_base=!output_base:-=!" + set "output_base=!output_base:.=!" + call :capitalize output_base + set "ns=!output_base!_!version_tag!_%%f" + set "file_tag=!version_tag!_%%f_!output_base!" + + call :generate "!file_path!" "!ns!.Disposable" ".\GeneratedCode\Disposable!file_tag!.generated.cs" "--disposable" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.ImmutableRecords" ".\GeneratedCode\ImmutableRecords!file_tag!.generated.cs" "--immutable-records" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.PolymorphicSerialization" ".\GeneratedCode\PolymorphicSerialization!file_tag!.generated.cs" "--use-polymorphic-serialization" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.CollectionFormatCsv" ".\GeneratedCode\CollectionFormatCsv!file_tag!.generated.cs" "--collection-format csv" "%process_path%" "%use_docker_param%" ) ) ) ) -:: Process URLs +:: V3.1 netCore variants +for %%f in (json yaml) do ( + for /l %%i in (0,1,0) do ( + set "cur_filename=!v31filename%%i!" + set "file_path=.\OpenAPI\v3.1\!cur_filename!.%%f" + + if exist "!file_path!" ( + set "output_base=!cur_filename!" + set "output_base=!output_base:-=!" + set "output_base=!output_base:.=!" + call :capitalize output_base + set "ns=!output_base!_v31_%%f" + set "file_tag=v31_%%f_!output_base!" + + call :generate "!file_path!" "!ns!.Disposable" ".\GeneratedCode\Disposable!file_tag!.generated.cs" "--disposable" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.ImmutableRecords" ".\GeneratedCode\ImmutableRecords!file_tag!.generated.cs" "--immutable-records" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.PolymorphicSerialization" ".\GeneratedCode\PolymorphicSerialization!file_tag!.generated.cs" "--use-polymorphic-serialization" "%process_path%" "%use_docker_param%" + call :generate "!file_path!" "!ns!.CollectionFormatCsv" ".\GeneratedCode\CollectionFormatCsv!file_tag!.generated.cs" "--collection-format csv" "%process_path%" "%use_docker_param%" + ) + ) +) + +:: ========================================== +:: Phase 6: Build netCore variants +:: ========================================== +echo. +echo === Building netCore variants === +echo. +call :build_solution ".\ConsoleApp\ConsoleApp.Core.slnx" "true" "true" + +:: ========================================== +:: Phase 7: URL-based tests (network-dependent) +:: ========================================== +echo. +echo === URL-based tests === +echo. +call :clean_generated_code + for %%u in ("https://petstore3.swagger.io/api/v3/openapi.json" "https://petstore3.swagger.io/api/v3/openapi.yaml") do ( set "namespace=PetstoreFromUri" set "output_path=PetstoreFromUri.generated.cs" - del /q "*.generated.cs" 2>nul - - set "process_path=.\bin\refitter" - if /i "%build_from_source_param%"=="false" set "process_path=refitter" - if /i "%use_docker_param%"=="true" set "process_path=docker" + del /q ".\GeneratedCode\*.cs" 2>nul if /i "%use_docker_param%"=="true" ( set "current_dir=%CD%" @@ -219,11 +398,7 @@ for %%u in ("https://petstore3.swagger.io/api/v3/openapi.json" "https://petstore call :throw_on_native_failure ) - echo. - echo Building ConsoleApp - echo. - dotnet build .\ConsoleApp\ConsoleApp.slnx - call :throw_on_native_failure + call :build_solution ".\ConsoleApp\ConsoleApp.slnx" "true" ) goto :eof @@ -240,6 +415,8 @@ for %%a in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) do ( set "%1=!first_char!!rest_chars!" goto :eof +:start_tests + :: Main execution if /i "%USE_PRODUCTION%"=="true" ( echo Running smoke tests in production mode @@ -261,11 +438,11 @@ echo Starting smoke tests... set "start_time=%time%" if /i "%USE_DOCKER%"=="true" ( - call :run_tests "dotnet-run" "%PARALLEL%" "false" "true" + call :run_tests "false" "true" ) else if /i "%USE_PRODUCTION%"=="true" ( - call :run_tests "dotnet-run" "%PARALLEL%" "false" "false" + call :run_tests "false" "false" ) else ( - call :run_tests "dotnet-run" "%PARALLEL%" "true" "false" + call :run_tests "true" "false" ) set "end_time=%time%" diff --git a/test/smoke-tests.ps1 b/test/smoke-tests.ps1 index aa434e121..e4d48c27d 100644 --- a/test/smoke-tests.ps1 +++ b/test/smoke-tests.ps1 @@ -1,15 +1,16 @@ param ( - [Parameter(Mandatory=$false)] - [bool] - $Parallel = $true, - [Parameter(Mandatory=$false)] [switch] $UseProduction = $false, [Parameter(Mandatory=$false)] [switch] - $UseDocker = $false + $UseDocker = $false, + + # Kept for backward compatibility + [Parameter(Mandatory=$false)] + [bool] + $Parallel = $true ) function ThrowOnNativeFailure @@ -20,171 +21,124 @@ function ThrowOnNativeFailure } } -function GenerateAndBuild +function GetProcessPath([bool]$buildFromSource, [bool]$useDocker) { - param ( - [Parameter(Mandatory=$true)] - [string] - $format, - - [Parameter(Mandatory=$true)] - [string] - $namespace, - - [Parameter(Mandatory=$false)] - [string] - $outputPath, - - [Parameter(Mandatory=$false)] - [string] - $args, - - [Parameter(Mandatory=$false)] - [switch] - $netCore = $false, - - [Parameter(Mandatory=$false)] - [string] - $csproj, - - [Parameter(Mandatory=$false)] - [bool] - $buildFromSource = $true, - - [Parameter(Mandatory=$false)] - [bool] - $useDocker = $false - ) + if ($useDocker) { return "docker" } + if (-not $buildFromSource) { return "refitter" } + return "./bin/refitter" +} - try - { - Get-ChildItem './GeneratedCode/*.cs' -Recurse | ForEach-Object { Remove-Item -Path $_.FullName -Force } - } catch - { - # Do nothing +function BuildDockerPrefix() +{ + $currentDir = (Get-Location).Path.Replace('\', '/') + $userParam = "" + if ($IsLinux -or $IsMacOS) { + $uid = sh -c 'id -u' + $gid = sh -c 'id -g' + $userParam = "--user ${uid}:${gid}" } + $prefix = "run --rm -v ""${currentDir}:/src"" -w /src" + if ($userParam) { $prefix += " $userParam" } + $prefix += " christianhelle/refitter" + return $prefix +} - $processPath = "./bin/refitter" - if ($buildFromSource -eq $false) - { - $processPath = "refitter" - } - if ($useDocker) - { - $processPath = "docker" - } +function StartRefitter +{ + param ( + [string]$arguments, + [string]$processPath, + [bool]$useDocker = $false + ) if ($useDocker) { - $currentDir = (Get-Location).Path.Replace('\', '/') - # Get current user ID and group ID for Linux/macOS - $userParam = "" - if ($IsLinux -or $IsMacOS) { - $uid = sh -c 'id -u' - $gid = sh -c 'id -g' - $userParam = "--user ${uid}:${gid}" - } - - if ($args.Contains("settings-file")) - { - if ($userParam) { - Write-Host "docker run --rm -v ""${currentDir}:/src"" -w /src $userParam christianhelle/refitter --no-logging $args" - $process = Start-Process $processPath ` - -Args "run --rm -v ""${currentDir}:/src"" -w /src $userParam christianhelle/refitter --no-logging $args" ` - -NoNewWindow ` - -PassThru - } else { - Write-Host "docker run --rm -v ""${currentDir}:/src"" -w /src christianhelle/refitter --no-logging $args" - $process = Start-Process $processPath ` - -Args "run --rm -v ""${currentDir}:/src"" -w /src christianhelle/refitter --no-logging $args" ` - -NoNewWindow ` - -PassThru - } - } else - { - if ($userParam) { - Write-Host "docker run --rm -v ""${currentDir}:/src"" -w /src $userParam christianhelle/refitter ./openapi.$format --namespace $namespace --output ./GeneratedCode/$outputPath --no-logging $args" - $process = Start-Process $processPath ` - -Args "run --rm -v ""${currentDir}:/src"" -w /src $userParam christianhelle/refitter ./openapi.$format --namespace $namespace --output ./GeneratedCode/$outputPath --no-logging $args" ` - -NoNewWindow ` - -PassThru - } else { - Write-Host "docker run --rm -v ""${currentDir}:/src"" -w /src christianhelle/refitter ./openapi.$format --namespace $namespace --output ./GeneratedCode/$outputPath --no-logging $args" - $process = Start-Process $processPath ` - -Args "run --rm -v ""${currentDir}:/src"" -w /src christianhelle/refitter ./openapi.$format --namespace $namespace --output ./GeneratedCode/$outputPath --no-logging $args" ` - -NoNewWindow ` - -PassThru - } - } + $dockerPrefix = BuildDockerPrefix + $fullArgs = "$dockerPrefix $arguments" + Write-Host "docker $fullArgs" + return Start-Process "docker" -Args $fullArgs -NoNewWindow -PassThru } - elseif ($args.Contains("settings-file")) - { - Write-Host "refitter --no-logging $args" - $process = Start-Process $processPath ` - -Args "--no-logging $args" ` - -NoNewWindow ` - -PassThru - } else + else { - Write-Host "refitter ./openapi.$format --namespace $namespace --output ./GeneratedCode/$outputPath --no-logging $args" - $process = Start-Process $processPath ` - -Args "./openapi.$format --namespace $namespace --output ./GeneratedCode/$outputPath --no-logging $args" ` - -NoNewWindow ` - -PassThru + Write-Host "$processPath $arguments" + return Start-Process $processPath -Args $arguments -NoNewWindow -PassThru } +} - $process | Wait-Process - if ($process.ExitCode -ne 0) - { - throw "Refitter failed" - } +function GenerateFromSettingsFile +{ + param ( + [string]$settingsFile, + [string]$processPath, + [bool]$useDocker = $false + ) - if ($csproj -ne '') - { - Write-Host "`r`nBuilding $csproj file`r`n" - $solution = $csproj - } else - { - Write-Host "`r`nBuilding ConsoleApp`r`n" - $solution = "./ConsoleApp/ConsoleApp.slnx" - if ($netCore) - { - $solution = "./ConsoleApp/ConsoleApp.Core.slnx" + $p = StartRefitter ` + -arguments "--no-logging --settings-file $settingsFile" ` + -processPath $processPath ` + -useDocker $useDocker + $p | Wait-Process + if ($p.ExitCode -ne 0) { throw "Refitter failed for settings file: $settingsFile" } +} + +function BuildSolution +{ + param ( + [string]$solution, + [switch]$noRestore, + [switch]$smokeTest + ) + + $buildArgs = "build $solution --nologo -v q --property WarningLevel=0 /clp:ErrorsOnly" + if ($noRestore) { $buildArgs += " --no-restore" } + if ($smokeTest) { $buildArgs += " --property:SmokeTest=true" } + + Write-Host "`r`nBuilding $solution`r`n" + $p = Start-Process "dotnet" -Args $buildArgs -NoNewWindow -PassThru + $p | Wait-Process + if ($p.ExitCode -ne 0) { throw "Build Failed: $solution" } +} + +function CleanGeneratedCode +{ + try { + if (Test-Path './GeneratedCode') { + Get-ChildItem './GeneratedCode' -Recurse -Include '*.cs' -ErrorAction SilentlyContinue | + ForEach-Object { Remove-Item -Path $_.FullName -Force } + Get-ChildItem './GeneratedCode' -Directory -ErrorAction SilentlyContinue | + ForEach-Object { Remove-Item -Path $_.FullName -Recurse -Force } } - } + } catch { } +} + +function RunGenerationTasks +{ + param ( + [array]$tasks, + [string]$processPath, + [bool]$useDocker + ) - $process = Start-Process "dotnet" ` - -Args "build $solution --nologo -v q --property WarningLevel=0 /clp:ErrorsOnly" ` - -NoNewWindow ` - -PassThru - $process | Wait-Process - if ($process.ExitCode -ne 0) + for ($i = 0; $i -lt $tasks.Count; $i++) { - throw "Build Failed!" + $task = $tasks[$i] + $arguments = "$($task.SpecPath) --namespace $($task.Namespace) --output $($task.OutputPath) --no-logging" + if ($task.Args) { $arguments += " $($task.Args)" } + $p = StartRefitter -arguments $arguments -processPath $processPath -useDocker $useDocker + $p | Wait-Process + if ($p.ExitCode -ne 0) { throw "Refitter generation failed for: $($task.SpecPath) ($($task.Namespace))" } } } function RunTests { param ( - [Parameter(Mandatory=$true)] - [ValidateSet("dotnet-run", "refitter")] - [string] - $Method, - - [Parameter(Mandatory=$false)] - [bool] - $Parallel = $false, - - [Parameter(Mandatory=$false)] - [bool] - $BuildFromSource = $true, - - [Parameter(Mandatory=$false)] - [bool] - $UseDocker = $false + [bool]$BuildFromSource = $true, + [bool]$UseDocker = $false ) + $processPath = GetProcessPath -buildFromSource $BuildFromSource -useDocker $UseDocker + $filenames = @( "weather", "bot.paths", @@ -202,129 +156,293 @@ function RunTests "hubspot-webhooks" ) + $v31Filenames = @( + "webhook-example" + ) + + # Standard variants: compile on all frameworks (net462-net10) + $standardVariants = @( + @{ Suffix="Cancellation"; Prefix="WithCancellation"; Args="--cancellation-tokens" }, + @{ Suffix="Internal"; Prefix="Internal"; Args="--internal" }, + @{ Suffix="UsingApiResponse"; Prefix="IApi"; Args="--use-api-response" }, + @{ Suffix="UsingIObservable"; Prefix="IObservable"; Args="--use-observable-response" }, + @{ Suffix="UsingIsoDateFormat"; Prefix="UsingIsoDateFormat"; Args="--use-iso-date-format" }, + @{ Suffix="MultipleInterfaces"; Prefix="MultipleInterfaces"; Args="--multiple-interfaces ByEndpoint" }, + # NOTE: --multiple-interfaces ByEndpoint --operation-name-template produces duplicate types per-endpoint. + # This is a known Refitter limitation. We test generation works but skip compilation. + # @{ Suffix="MultipleInterfaces"; Prefix="MultipleInterfacesWithCustomName"; Args="--multiple-interfaces ByEndpoint --operation-name-template ExecuteAsync" }, + @{ Suffix="ContractOnly"; Prefix="ContractOnly"; Args="--contract-only" }, + @{ Suffix="DynamicQuerystring"; Prefix="DynamicQuerystring"; Args="--use-dynamic-querystring-parameters" }, + @{ Suffix="IntegerTypeInt64"; Prefix="IntegerTypeInt64"; Args="--integer-type Int64" }, + @{ Suffix="TrimUnusedSchema"; Prefix="TrimUnusedSchema"; Args="--trim-unused-schema" }, + @{ Suffix="OptionalNullable"; Prefix="OptionalNullable"; Args="--optional-nullable-parameters" }, + @{ Suffix="NoDeprecated"; Prefix="NoDeprecated"; Args="--no-deprecated-operations" }, + @{ Suffix="NoAutoGeneratedHeader"; Prefix="NoAutoGenHeader"; Args="--no-auto-generated-header" }, + @{ Suffix="NoAcceptHeaders"; Prefix="NoAcceptHeaders"; Args="--no-accept-headers" }, + @{ Suffix="SkipDefaultAdditionalProps"; Prefix="SkipDefaultAddlProps"; Args="--skip-default-additional-properties" }, + @{ Suffix="NoInlineJsonConverters"; Prefix="NoInlineJsonConv"; Args="--no-inline-json-converters" } + ) + + # Petstore-only variants: require specs with specific tags/paths (petstore has "pet", "user", "store" tags) + $petstoreOnlyVariants = @( + @{ Suffix="TagFiltered"; Prefix="TagFiltered"; Args="--tag pet --tag user --tag store" }, + @{ Suffix="MatchPathFiltered"; Prefix="MatchPathFiltered"; Args="--match-path ^/pet/.*" }, + @{ Suffix="MultipleInterfacesByTag"; Prefix="MultipleInterfacesByTag"; Args="--multiple-interfaces ByTag" } + ) + + # NetCore variants: require net8.0+ features + $netCoreVariants = @( + @{ Suffix="Disposable"; Prefix="Disposable"; Args="--disposable" }, + @{ Suffix="ImmutableRecords"; Prefix="ImmutableRecords"; Args="--immutable-records" }, + @{ Suffix="PolymorphicSerialization"; Prefix="PolymorphicSerialization"; Args="--use-polymorphic-serialization" }, + @{ Suffix="CollectionFormatCsv"; Prefix="CollectionFormatCsv"; Args="--collection-format csv" } + ) + + # ========================================== + # Phase 0: Build refitter from source + # ========================================== if ($BuildFromSource -and -not $UseDocker) { - Write-Host "dotnet publish ../src/Refitter/Refitter.csproj -c Release -o bin -f net10.0" - Start-Process "dotnet" -Args "publish ../src/Refitter/Refitter.csproj -c Release -o bin -f net10.0" -NoNewWindow -PassThru | Wait-Process + Write-Host "dotnet publish ../src/Refitter/Refitter.csproj -c Release -o bin -f net9.0" + Start-Process "dotnet" -Args "publish ../src/Refitter/Refitter.csproj -c Release -o bin -f net9.0" -NoNewWindow -PassThru | Wait-Process Write-Host "refitter --version" - $process = Start-Process "./bin/refitter" ` - -Args " --version" ` - -NoNewWindow ` - -PassThru - $process | Wait-Process - if ($process.ExitCode -ne 0) - { - throw "Show version failed!" - } + $p = Start-Process "./bin/refitter" -Args "--version" -NoNewWindow -PassThru + $p | Wait-Process + if ($p.ExitCode -ne 0) { throw "Show version failed!" } } - GenerateAndBuild -format " " -namespace " " -outputPath "SwaggerPetstoreDirect.generated.cs" -args "--settings-file ./petstore.refitter" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format " " -namespace " " -args "--settings-file ./Apizr/petstore.apizr.refitter" -csproj "./Apizr/Sample.csproj" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format " " -namespace " " -args "--settings-file ./MultipleFiles/petstore.refitter" -csproj "MultipleFiles/Client/Client.csproj" -buildFromSource $buildFromSource -useDocker $UseDocker - - "v3.0", "v2.0" | ForEach-Object { - $version = $_ - "json", "yaml" | ForEach-Object { - $format = $_ - $filenames | ForEach-Object { - $filename = "./OpenAPI/$version/$_.$format" - $exists = Test-Path -Path $filename -PathType Leaf - if ($exists -eq $true) + # ========================================== + # Phase 1: Pre-restore packages + # ========================================== + Write-Host "`r`n=== Pre-restoring packages ===`r`n" + Start-Process "dotnet" -Args "restore ./ConsoleApp/ConsoleApp.slnx --nologo -v q" -NoNewWindow -PassThru | Wait-Process + Start-Process "dotnet" -Args "restore ./ConsoleApp/ConsoleApp.Core.slnx --nologo -v q" -NoNewWindow -PassThru | Wait-Process + Start-Process "dotnet" -Args "restore ./Apizr/Sample.csproj --nologo -v q" -NoNewWindow -PassThru | Wait-Process + + # ========================================== + # Phase 2: Settings-file tests (individual generate + build) + # ========================================== + Write-Host "`r`n=== Settings-file tests ===`r`n" + + CleanGeneratedCode + GenerateFromSettingsFile -settingsFile "./petstore.refitter" -processPath $processPath -useDocker $UseDocker + BuildSolution -solution "./ConsoleApp/ConsoleApp.slnx" -noRestore + + CleanGeneratedCode + GenerateFromSettingsFile -settingsFile "./Apizr/petstore.apizr.refitter" -processPath $processPath -useDocker $UseDocker + BuildSolution -solution "./Apizr/Sample.csproj" -noRestore + + GenerateFromSettingsFile -settingsFile "./MultipleFiles/petstore.refitter" -processPath $processPath -useDocker $UseDocker + BuildSolution -solution "MultipleFiles/Client/Client.csproj" + + CleanGeneratedCode + GenerateFromSettingsFile -settingsFile "./multiple-sources.refitter" -processPath $processPath -useDocker $UseDocker + BuildSolution -solution "./ConsoleApp/ConsoleApp.Core.slnx" -noRestore + + # ========================================== + # Phase 3: Generate all STANDARD variants (no build until all are generated) + # ========================================== + Write-Host "`r`n=== Generating standard variants ===`r`n" + CleanGeneratedCode + + $standardTasks = @() + $netCoreTasks = @() + + # Helper to create unique file tag from version/format/filename + function MakeFileTag([string]$version, [string]$format, [string]$filename) + { + $vTag = $version.Replace(".", "") + $base = $filename.Replace("-", "").Replace(".", "") + $base = $base.Substring(0, 1).ToUpperInvariant() + $base.Substring(1) + $nsBase = "${base}_${vTag}_${format}" + return @{ Tag = "${vTag}_${format}_${base}"; Namespace = $nsBase } + } + + # Collect generation tasks for v2.0 and v3.0 + foreach ($version in @("v3.0", "v2.0")) + { + foreach ($format in @("json", "yaml")) + { + foreach ($filename in $filenames) + { + $specPath = "./OpenAPI/$version/$filename.$format" + if (-not (Test-Path -Path $specPath -PathType Leaf)) { continue } + + $info = MakeFileTag $version $format $filename + $fileTag = $info.Tag + $ns = $info.Namespace + + foreach ($v in $standardVariants) { - Copy-Item $filename ./openapi.$format - $outputPath = "$_.generated.cs" - $outputPath = $outputPath.Substring(0, 1).ToUpperInvariant() + $outputPath.Substring(1, $outputPath.Length - 1) - $namespace = $_.Replace("-", "") - $namespace = $namespace.Substring(0, 1).ToUpperInvariant() + $namespace.Substring(1, $namespace.Length - 1) - - GenerateAndBuild -format $format -namespace "$namespace.Disposable" -outputPath "Disposable$outputPath" -args "--disposable" -buildFromSource $buildFromSource -netCore -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.MultipleFiles" -args "--multiple-files" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.SeparateContractsFile" -args "--contracts-output GeneratedCode/Contracts --contracts-namespace $namespace.SeparateContractsFile.Contracts" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.Cancellation" -outputPath "WithCancellation$outputPath" "--cancellation-tokens" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.Internal" -outputPath "Internal$outputPath" -args "--internal" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.UsingApiResponse" -outputPath "IApi$outputPath" -args "--use-api-response" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.UsingIObservable" -outputPath "IObservable$outputPath" -args "--use-observable-response" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.UsingIsoDateFormat" -outputPath "UsingIsoDateFormat$outputPath" -args "--use-iso-date-format" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.MultipleInterfaces" -outputPath "MultipleInterfaces$outputPath" -args "--multiple-interfaces ByEndpoint" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.MultipleInterfaces" -outputPath "MultipleInterfacesWithCustomName$outputPath" -args "--multiple-interfaces ByEndpoint --operation-name-template ExecuteAsync" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.TagFiltered" -outputPath "TagFiltered$outputPath" -args "--tag pet --tag user --tag store" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.MatchPathFiltered" -outputPath "MatchPathFiltered$outputPath" -args "--match-path ^/pet/.*" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.ContractOnly" -outputPath "ContractOnly$outputPath" -args "--contract-only" -buildFromSource $buildFromSource -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.ImmutableRecords" -outputPath "ImmutableRecords$outputPath" -args "--immutable-records" -buildFromSource $buildFromSource -netCore -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.PolymorphicSerialization" -outputPath "PolymorphicSerialization$outputPath" -args "--use-polymorphic-serialization" -buildFromSource $buildFromSource -netCore -useDocker $UseDocker - GenerateAndBuild -format $format -namespace "$namespace.CollectionFormatCsv" -outputPath "CollectionFormatCsv$outputPath" -args "--collection-format csv" -buildFromSource $buildFromSource -netCore -useDocker $UseDocker + $standardTasks += @{ + SpecPath = $specPath + Namespace = "$ns.$($v.Suffix)" + OutputPath = "./GeneratedCode/$($v.Prefix)${fileTag}.generated.cs" + Args = $v.Args + } } - } - } - } - "https://petstore3.swagger.io/api/v3/openapi.json", "https://petstore3.swagger.io/api/v3/openapi.yaml" | ForEach-Object { - $namespace = "PetstoreFromUri" - $outputPath = "PetstoreFromUri.generated.cs" + # Petstore-only variants (tag/path filters require petstore-specific tags) + if ($filename -like "petstore*") + { + foreach ($v in $petstoreOnlyVariants) + { + $standardTasks += @{ + SpecPath = $specPath + Namespace = "$ns.$($v.Suffix)" + OutputPath = "./GeneratedCode/$($v.Prefix)${fileTag}.generated.cs" + Args = $v.Args + } + } + } - Get-ChildItem '*.generated.cs' -Recurse | foreach { Remove-Item -Path $_.FullName } + # Multiple files variant (unique subdirectory) + $standardTasks += @{ + SpecPath = $specPath + Namespace = "$ns.MultipleFiles" + OutputPath = "./GeneratedCode/MultipleFiles/$fileTag/" + Args = "--multiple-files" + } - $processPath = "./bin/refitter" - if ($buildFromSource -eq $false) - { - $processPath = "refitter" - } - if ($UseDocker) - { - $processPath = "docker" + # Separate contracts variant (unique subdirectories for both interface and contracts) + $standardTasks += @{ + SpecPath = $specPath + Namespace = "$ns.SeparateContractsFile" + OutputPath = "./GeneratedCode/SeparateContracts/$fileTag/" + Args = "--contracts-output GeneratedCode/Contracts/$fileTag --contracts-namespace $ns.SeparateContractsFile.Contracts" + } + + foreach ($v in $netCoreVariants) + { + $netCoreTasks += @{ + SpecPath = $specPath + Namespace = "$ns.$($v.Suffix)" + OutputPath = "./GeneratedCode/$($v.Prefix)${fileTag}.generated.cs" + Args = $v.Args + } + } + } } + } - if ($UseDocker) + # Collect generation tasks for v3.1 + # Note: v3.1 webhook specs may not have regular API paths, so skip MultipleInterfaces variants + foreach ($format in @("json", "yaml")) + { + foreach ($filename in $v31Filenames) { - $currentDir = (Get-Location).Path.Replace('\', '/') - # Get current user ID and group ID for Linux/macOS - $userParam = "" - if ($IsLinux -or $IsMacOS) { - $uid = sh -c 'id -u' - $gid = sh -c 'id -g' - $userParam = "--user ${uid}:${gid}" + $specPath = "./OpenAPI/v3.1/$filename.$format" + if (-not (Test-Path -Path $specPath -PathType Leaf)) { continue } + + $info = MakeFileTag "v3.1" $format $filename + $fileTag = $info.Tag + $ns = $info.Namespace + + foreach ($v in $standardVariants) + { + if ($v.Args -like "*--multiple-interfaces*") { continue } + $standardTasks += @{ + SpecPath = $specPath + Namespace = "$ns.$($v.Suffix)" + OutputPath = "./GeneratedCode/$($v.Prefix)${fileTag}.generated.cs" + Args = $v.Args + } } - - if ($userParam) { - Write-Host "docker run --rm -v ""${currentDir}:/src"" -w /src $userParam christianhelle/refitter ""$_"" --namespace $namespace --output ./GeneratedCode/$outputPath" - $process = Start-Process $processPath ` - -Args "run --rm -v ""${currentDir}:/src"" -w /src $userParam christianhelle/refitter ""$_"" --namespace $namespace --output ./GeneratedCode/$outputPath" ` - -NoNewWindow ` - -PassThru - } else { - Write-Host "docker run --rm -v ""${currentDir}:/src"" -w /src christianhelle/refitter ""$_"" --namespace $namespace --output ./GeneratedCode/$outputPath" - $process = Start-Process $processPath ` - -Args "run --rm -v ""${currentDir}:/src"" -w /src christianhelle/refitter ""$_"" --namespace $namespace --output ./GeneratedCode/$outputPath" ` - -NoNewWindow ` - -PassThru + + $standardTasks += @{ + SpecPath = $specPath + Namespace = "$ns.MultipleFiles" + OutputPath = "./GeneratedCode/MultipleFiles/$fileTag/" + Args = "--multiple-files" } - } - else - { - Write-Host "refitter ./openapi.$format --namespace $namespace --output ./GeneratedCode/$outputPath --no-logging $args" - $process = Start-Process $processPath ` - -Args """$_"" --namespace $namespace --output ./GeneratedCode/$outputPath" ` - -NoNewWindow ` - -PassThru - } - $process | Wait-Process - if ($process.ExitCode -ne 0) - { - throw "Refitter failed" - } - Write-Host "`r`nBuilding ConsoleApp`r`n" - $process = Start-Process "dotnet" ` - -Args "build ./ConsoleApp/ConsoleApp.slnx" ` - -NoNewWindow ` - -PassThru - $process | Wait-Process - if ($process.ExitCode -ne 0) - { - throw "Build Failed!" + $standardTasks += @{ + SpecPath = $specPath + Namespace = "$ns.SeparateContractsFile" + OutputPath = "./GeneratedCode/SeparateContracts/$fileTag/" + Args = "--contracts-output GeneratedCode/Contracts/$fileTag --contracts-namespace $ns.SeparateContractsFile.Contracts" + } + + foreach ($v in $netCoreVariants) + { + $netCoreTasks += @{ + SpecPath = $specPath + Namespace = "$ns.$($v.Suffix)" + OutputPath = "./GeneratedCode/$($v.Prefix)${fileTag}.generated.cs" + Args = $v.Args + } + } } } + + Write-Host "Standard generation tasks: $($standardTasks.Count)" + Write-Host "NetCore generation tasks: $($netCoreTasks.Count)" + + # Execute standard generation in parallel batches + RunGenerationTasks -tasks $standardTasks -processPath $processPath -useDocker $UseDocker + + # ========================================== + # Phase 4: Build standard variants (one build validates all) + # ========================================== + Write-Host "`r`n=== Building standard variants ===`r`n" + BuildSolution -solution "./ConsoleApp/ConsoleApp.slnx" -noRestore -smokeTest + + # ========================================== + # Phase 4b: Generate-only test for MultipleInterfacesWithCustomName + # This variant uses --multiple-interfaces ByEndpoint --operation-name-template which + # generates duplicate types per-endpoint (known limitation). We verify generation succeeds. + # ========================================== + Write-Host "`r`n=== Generate-only: MultipleInterfacesWithCustomName (petstore) ===`r`n" + $customNameSpec = "./OpenAPI/v3.0/petstore.json" + $customNameArgs = "--multiple-interfaces ByEndpoint --operation-name-template ExecuteAsync" + $customNameOutput = "./GeneratedCode/MultipleInterfacesWithCustomName_generateonly.cs" + $customNameCmd = "$processPath $customNameSpec --namespace GenerateOnly.MultipleInterfacesWithCustomName --output $customNameOutput --no-logging $customNameArgs" + Write-Host $customNameCmd + Invoke-Expression $customNameCmd + if (-not (Test-Path $customNameOutput)) { throw "Generate-only test failed: MultipleInterfacesWithCustomName" } + Remove-Item $customNameOutput -Force + Write-Host "Generate-only test passed: MultipleInterfacesWithCustomName" + + # ========================================== + # Phase 5: Generate netCore variants (accumulate on top of standard code) + # Net8/Net9/Net10 can compile both standard and netCore code + # ========================================== + Write-Host "`r`n=== Generating netCore variants ===`r`n" + RunGenerationTasks -tasks $netCoreTasks -processPath $processPath -useDocker $UseDocker + + # ========================================== + # Phase 6: Build netCore variants + # ========================================== + Write-Host "`r`n=== Building netCore variants ===`r`n" + BuildSolution -solution "./ConsoleApp/ConsoleApp.Core.slnx" -noRestore -smokeTest + + # ========================================== + # Phase 7: URL-based tests (network-dependent) + # ========================================== + Write-Host "`r`n=== URL-based tests ===`r`n" + CleanGeneratedCode + + @("https://petstore3.swagger.io/api/v3/openapi.json", "https://petstore3.swagger.io/api/v3/openapi.yaml") | ForEach-Object { + $url = $_ + $urlFormat = if ($url.EndsWith(".json")) { "json" } else { "yaml" } + $namespace = "PetstoreFromUri" + $outputPath = "PetstoreFromUri.generated.cs" + + try { + Get-ChildItem './GeneratedCode/*.cs' -Recurse -ErrorAction SilentlyContinue | + ForEach-Object { Remove-Item -Path $_.FullName -Force } + } catch { } + + $p = StartRefitter ` + -arguments """$url"" --namespace $namespace --output ./GeneratedCode/$outputPath --no-logging" ` + -processPath $processPath ` + -useDocker $UseDocker + $p | Wait-Process + if ($p.ExitCode -ne 0) { throw "Refitter failed for URL: $url" } + + BuildSolution -solution "./ConsoleApp/ConsoleApp.slnx" -noRestore + } } if ($UseProduction) @@ -345,6 +463,10 @@ if ($UseDocker) Write-Host "`r`n" } -Measure-Command { RunTests -Method "dotnet-run" -Parallel $Parallel -BuildFromSource (!$UseProduction -and !$UseDocker) -UseDocker $UseDocker } +Measure-Command { + RunTests ` + -BuildFromSource (!$UseProduction -and !$UseDocker) ` + -UseDocker $UseDocker +} Write-Host "`r`n" Write-Host "`r`n"