diff --git a/example/lib/main.dart b/example/lib/main.dart index 3de4cdb..8c102bf 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,3 @@ -// Openapi Generator last run: : 2024-10-31T23:11:13.130123 import 'package:flutter/material.dart'; import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; @@ -13,7 +12,7 @@ void main() { RemoteSpec(path: 'https://petstore3.swagger.io/api/v3/openapi.json'), typeMappings: {'Pet': 'ExamplePet'}, generatorName: Generator.dioAlt, - updateAnnotatedFile: true, + updateAnnotatedFile: false, runSourceGenOnOutput: true, outputDirectory: 'api/petstore_api', ) diff --git a/openapi-generator-cli/bin/main.dart b/openapi-generator-cli/bin/main.dart index b69e987..daf1f35 100644 --- a/openapi-generator-cli/bin/main.dart +++ b/openapi-generator-cli/bin/main.dart @@ -69,21 +69,29 @@ String constructJarUrl(String version) { } /// Downloads a JAR file to the specified output path if it doesn't already exist -Future downloadJar(String url, String outputPath) async { +Future downloadJar( + String url, + String outputPath, { + http.Client? client, // Injected HTTP client for testing + void Function(String message) log = + _logOutput, // Optional log function for testing +}) async { outputPath = resolvePath(outputPath); final file = File(outputPath); + client ??= + http.Client(); // Use the injected client or default to a new client + if (!await file.exists()) { - _logOutput('Downloading $url...'); + log('Downloading $url...'); final request = http.Request('GET', Uri.parse(url)); - final response = await request.send(); + final response = await client.send(request); if (response.statusCode == 200) { final contentLength = response.contentLength ?? 0; final output = file.openWrite(); var downloadedBytes = 0; - // Listen to the stream and write to the file in smaller chunks await response.stream.listen( (chunk) { downloadedBytes += chunk.length; @@ -92,15 +100,15 @@ Future downloadJar(String url, String outputPath) async { // Display progress if content length is known if (contentLength != 0) { final progress = (downloadedBytes / contentLength) * 100; - stdout.write('\rProgress: ${progress.toStringAsFixed(2)}%'); + log('\rProgress: ${progress.toStringAsFixed(2)}%'); } }, onDone: () async { await output.close(); - print('\nDownloaded to $outputPath\n'); + log('\nDownloaded to $outputPath\n'); }, onError: (e) { - print('\nDownload failed: $e\n'); + log('\nDownload failed: $e\n'); }, cancelOnError: true, ).asFuture(); @@ -109,13 +117,13 @@ Future downloadJar(String url, String outputPath) async { 'Failed to download $url. Status code: ${response.statusCode}'); } } else { - print('[info] $outputPath found. No need to download'); + log('[info] $outputPath found. No need to download'); } } /// Executes the OpenAPI Generator using all JARs in the classpath -Future executeWithClasspath( - List jarPaths, List arguments) async { +Future executeWithClasspath(List jarPaths, List arguments, + [ProcessRunner process = const ProcessRunner()]) async { final javaOpts = Platform.environment['JAVA_OPTS'] ?? ''; final classpath = jarPaths.join(Platform.isWindows ? ';' : ':'); final commands = [ @@ -129,14 +137,31 @@ Future executeWithClasspath( commands.insert(0, javaOpts); } - final result = await Process.run('java', commands); + final result = + await process.run('java', commands, runInShell: Platform.isWindows); print(result.stdout); print(result.stderr); } -/// Main function handling config loading, JAR downloading, and command execution Future main(List arguments) async { - exitCode = 0; // presume success + await runMain( + arguments: arguments, + loadConfig: loadOrCreateConfig, + downloadJar: downloadJar, + executeWithClasspath: executeWithClasspath, + log: _logOutput, + ); +} + +Future runMain({ + required List arguments, + required Future> Function(String) loadConfig, + required Future Function(String, String) downloadJar, + required Future Function(List, List) + executeWithClasspath, + required void Function(String) log, +}) async { + exitCode = 0; // Determine config path from arguments or default to 'openapi_generator_config.json' final configArgIndex = arguments.indexOf('--config'); @@ -145,22 +170,21 @@ Future main(List arguments) async { ? arguments[configArgIndex + 1] : 'openapi_generator_config.json'; - print('Using config file: $configFilePath'); - - final config = await loadOrCreateConfig(configFilePath); - final String version = (config[ConfigKeys.openapiGeneratorVersion] ?? - ConfigDefaults.openapiGeneratorVersion); - final String additionalCommands = config[ConfigKeys.additionalCommands] ?? - ConfigDefaults.additionalCommands; - final String? overrideUrl = config[ConfigKeys.downloadUrlOverride]; - final cachePath = resolvePath( - config[ConfigKeys.jarCachePath] ?? ConfigDefaults.jarCacheDir); + log('Using config file: $configFilePath'); - final customGeneratorUrls = List.from( - config[ConfigKeys.customGeneratorUrls] ?? - ConfigDefaults.customGeneratorUrls); try { - // Load or create configuration + final config = await loadConfig(configFilePath); + final version = config[ConfigKeys.openapiGeneratorVersion] ?? + ConfigDefaults.openapiGeneratorVersion; + final additionalCommands = config[ConfigKeys.additionalCommands] ?? + ConfigDefaults.additionalCommands; + final overrideUrl = config[ConfigKeys.downloadUrlOverride]; + final cachePath = resolvePath( + config[ConfigKeys.jarCachePath] ?? ConfigDefaults.jarCacheDir); + + final customGeneratorUrls = List.from( + config[ConfigKeys.customGeneratorUrls] ?? + ConfigDefaults.customGeneratorUrls); // Ensure the cache directory exists await Directory(cachePath).create(recursive: true); @@ -173,15 +197,14 @@ Future main(List arguments) async { await downloadJar(overrideUrl ?? constructJarUrl(version), openapiJarPath); // Download each custom generator JAR if it doesn't exist and store in `customJarPaths` - for (var i = 0; i < customGeneratorUrls.length; i++) { - final customJarUrl = customGeneratorUrls[i]; + for (var customJarUrl in customGeneratorUrls) { final originalFileName = customJarUrl.split('/').last; final customJarPath = '$cachePath/custom-$originalFileName'; await downloadJar(customJarUrl, customJarPath); customJarPaths.add(customJarPath); } - // Combine all JAR paths (OpenAPI Generator + custom generators) for the classpath + // Combine all JAR paths for the classpath final jarPaths = [openapiJarPath, ...customJarPaths]; // Prepare additional arguments, excluding the --config flag and its value @@ -190,19 +213,30 @@ Future main(List arguments) async { ...arguments.where((arg) => arg != '--config' && arg != configFilePath), ]; - // Execute using all JARs in the classpath + // Execute with classpath await executeWithClasspath( - jarPaths, - filteredArguments - .map( - (e) => e.trim(), - ) - .where( - (element) => element.isNotEmpty, - ) - .toList()); + jarPaths, + filteredArguments + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(), + ); } catch (e) { - _logOutput('Error: $e'); + log('Error: $e'); exitCode = 1; } } + +class ProcessRunner { + const ProcessRunner(); + + Future run(String executable, List arguments, + {Map? environment, + String? workingDirectory, + bool runInShell = false}) { + return Process.run(executable, arguments, + environment: environment, + workingDirectory: workingDirectory, + runInShell: runInShell); + } +} diff --git a/openapi-generator-cli/test/openapi_generator_cli_test.dart b/openapi-generator-cli/test/openapi_generator_cli_test.dart index 89bc8c7..218391c 100644 --- a/openapi-generator-cli/test/openapi_generator_cli_test.dart +++ b/openapi-generator-cli/test/openapi_generator_cli_test.dart @@ -1,7 +1,229 @@ +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; +import '../bin/main.dart'; + void main() { - test('calculate', () { -// expect(calculate(), 42); + late Directory tempDir; + late String configFilePath; + late String jarFilePath; + late String customJarFilePath; + + setUp(() async { + // Set up a temporary directory for testing + tempDir = await Directory.systemTemp.createTemp('openapi_generator_test'); + configFilePath = p.join(tempDir.path, 'openapi_generator_config.json'); + jarFilePath = p.join(tempDir.path, 'openapi-generator-cli-test.jar'); + customJarFilePath = + p.join(tempDir.path, 'custom-openapi-dart-generator.jar'); + }); + + tearDown(() async { + // Clean up any files or directories created during tests + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + }); + + test( + 'loadOrCreateConfig creates a config file with default values if not found', + () async { + final config = await loadOrCreateConfig(configFilePath); + + // Check that the file was created with default values + expect(config[ConfigKeys.openapiGeneratorVersion], + ConfigDefaults.openapiGeneratorVersion); + expect(config[ConfigKeys.additionalCommands], + ConfigDefaults.additionalCommands); + expect(config[ConfigKeys.downloadUrlOverride], + ConfigDefaults.downloadUrlOverride); + expect(config[ConfigKeys.jarCachePath], ConfigDefaults.jarCacheDir); + + // Ensure the file exists and contains the correct JSON structure + final configFile = File(configFilePath); + expect(await configFile.exists(), isTrue); + final contents = await configFile.readAsString(); + expect(contents, contains(ConfigDefaults.openapiGeneratorVersion)); + }); + + test('downloadJar downloads a JAR file when it does not exist', () async { + // Mock the HTTP client to avoid real network calls + final mockClient = MockClient((request) async { + return http.Response.bytes( + List.filled(1024, 1), 200); // 1 KB of dummy data + }); + http.Client client = mockClient; + + await downloadJar(constructJarUrl('test'), jarFilePath, client: client); + + // Verify the file was downloaded + final jarFile = File(jarFilePath); + expect(await jarFile.exists(), isTrue); + expect(await jarFile.length(), greaterThan(0)); + }); + + test('downloadJar does not download if file already exists', () async { + // Create an empty file at the target path + final file = await File(jarFilePath).create(); + await file.writeAsString('existing file content'); + final mockClient = MockClient((request) async { + fail('HTTP client should not be called when file exists'); + return http.Response.bytes( + List.filled(1024, 1), 200); // 1 KB of dummy data + }); + http.Client client = mockClient; + await downloadJar(constructJarUrl('test'), jarFilePath, client: client); + // Verify that the file content was not overwritten + final content = await file.readAsString(); + expect(content, equals('existing file content')); + }); + + test('executeWithClasspath runs the process with all JARs in the classpath', + () async { + // Mock the HTTP client to avoid real network calls + final mockClient = MockClient((request) async { + return http.Response.bytes( + List.filled(1024, 1), 200); // 1 KB of dummy data + }); + + final jarPaths = [jarFilePath, customJarFilePath]; + final args = []; + final javaOpts = Platform.environment['JAVA_OPTS'] ?? ''; + final classpath = jarPaths.join(Platform.isWindows ? ';' : ':'); + var commands = [ + '-cp', + classpath, + 'org.openapitools.codegen.OpenAPIGenerator', + ...args, + ]; + if (javaOpts.isNotEmpty) { + commands.insert(0, javaOpts); + } + // Run the process with the JARs in the classpath + await executeWithClasspath(jarPaths, args, TestProcessRunner( + runDelegate: (executable, arguments, + {environment, runInShell, workingDirectory}) async { + expect(executable, 'java'); + expect(arguments, commands); + expect(runInShell, Platform.isWindows); + return ProcessResult(1, 0, null, null); + }, + )); + }); + + test('constructJarUrl constructs the correct URL', () { + final version = '7.9.0'; + final expectedUrl = + 'https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.9.0/openapi-generator-cli-7.9.0.jar'; + expect(constructJarUrl(version), equals(expectedUrl)); }); + + test('runMain successfully loads config and calls required methods', + () async { + final logBuffer = StringBuffer(); + final mockConfigPath = 'mock_config.json'; + final mockVersion = '1.0.0'; + final mockCacheDir = '.dart_tool/openapi_generator_cache'; + final mockArguments = ['--config', mockConfigPath, 'generate']; + + // Mock functions + Future> mockLoadConfig(String path) async { + expect(path, mockConfigPath); + return { + ConfigKeys.openapiGeneratorVersion: mockVersion, + ConfigKeys.jarCachePath: mockCacheDir, + ConfigKeys.customGeneratorUrls: [], + }; + } + + Future mockDownloadJar(String url, String outputPath) async { + expect(url, contains(mockVersion)); // Ensure URL is correctly constructed + expect(outputPath, contains(mockCacheDir)); // Check output path location + } + + Future mockExecuteWithClasspath( + List jarPaths, + List args, + ) async { + expect(jarPaths, isNotEmpty); // JARs should be included in classpath + expect(args, contains('generate')); // Check arguments passed correctly + } + + void mockLog(String message) { + logBuffer.writeln(message); // Capture log messages + } + + // Run the test + await runMain( + arguments: mockArguments, + loadConfig: mockLoadConfig, + downloadJar: mockDownloadJar, + executeWithClasspath: mockExecuteWithClasspath, + log: mockLog, + ); + + // Verify log output + expect( + logBuffer.toString(), contains('Using config file: $mockConfigPath')); + }); + + test('runMain handles errors gracefully and sets exitCode to 1', () async { + final logBuffer = StringBuffer(); + final mockArguments = ['--config', 'nonexistent_config.json']; + + // Mock functions + Future> mockLoadConfig(String path) async { + throw FileSystemException('File not found', path); + } + + Future mockDownloadJar(String url, String outputPath) async { + fail('downloadJar should not be called on error'); + } + + Future mockExecuteWithClasspath( + List jarPaths, List args) async { + fail('executeWithClasspath should not be called on error'); + } + + void mockLog(String message) { + logBuffer.writeln(message); + } + + // Run the test with error + await runMain( + arguments: mockArguments, + loadConfig: mockLoadConfig, + downloadJar: mockDownloadJar, + executeWithClasspath: mockExecuteWithClasspath, + log: mockLog, + ); + + // Verify the exit code and log output + expect(exitCode, equals(1)); + expect(logBuffer.toString(), contains('Error: FileSystemException')); + }); +} + +class TestProcessRunner extends ProcessRunner { + Future Function(String executable, List arguments, + {Map? environment, + String? workingDirectory, + bool? runInShell}) runDelegate; + + TestProcessRunner({required this.runDelegate}); + + @override + Future run(String executable, List arguments, + {Map? environment, + String? workingDirectory, + bool runInShell = false}) { + return runDelegate.call(executable, arguments, + environment: environment, + workingDirectory: workingDirectory, + runInShell: runInShell); + } }