From 951296162cb5f064538b9800f4ea53cfb8d43060 Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Tue, 6 Oct 2026 16:23:26 +0200 Subject: [PATCH 1/9] Extract Python guide infrastructure for independent master review Preserve committed source and existing tests; separate shared prerequisites from guide changes. Co-authored-by: Graeme Rocher Co-authored-by: Nemanja Mikic Co-authored-by: Nemanja Mikic Co-authored-by: Sergio del Amo --- .github/workflows/python-guides.yml | 49 ++++ README.md | 16 ++ buildSrc/build.gradle | 4 +- buildSrc/settings.gradle | 10 + .../guides/GuideProjectGenerator.groovy | 168 +++++++++-- .../io/micronaut/guides/GuidesPlugin.groovy | 262 ++++++++++++++++-- .../io/micronaut/guides/IndexGenerator.groovy | 17 +- .../guides/TestScriptGenerator.groovy | 37 ++- .../guides/tasks/PythonTestScriptTask.groovy | 44 +++ .../tasks/SampleProjectGenerationTask.groovy | 26 +- .../tasks/TestScriptRunnerWorkAction.groovy | 2 +- .../guides/tasks/TestScriptTask.groovy | 4 +- .../java/io/micronaut/guides/core/App.java | 43 ++- .../core/DefaultFilesTransferUtility.java | 103 +++++-- .../guides/core/DefaultGuideParser.java | 25 +- .../core/DefaultGuideProjectGenerator.java | 28 +- .../core/DefaultTestScriptGenerator.java | 53 +++- .../guides/core/DependencyLines.java | 60 +++- .../java/io/micronaut/guides/core/Guide.java | 37 ++- .../guides/core/GuideGenerationUtils.java | 11 + .../io/micronaut/guides/core/GuideUtils.java | 49 +++- .../io/micronaut/guides/core/MacroUtils.java | 22 ++ .../core/SourceBlockMacroSubstitution.java | 47 +++- .../guides/core/TestScriptGenerator.java | 4 + .../core/ZipIncludeMacroSubstitution.java | 3 +- .../micronaut/guides/feature/H2Runtime.java | 11 + .../guides/feature/SpringSecurityCrypto.java | 27 ++ .../main/resources/guide-metadata.schema.json | 17 +- buildSrc/src/main/resources/pom.xml | 4 + .../main/resources/pyronaut-test-functions.sh | 223 +++++++++++++++ .../micronaut/guides/DocsOnlyGuideTest.java | 133 +++++++++ .../guides/GuideAsciidocGeneratorTest.java | 40 ++- .../guides/GuideProjectGeneratorTest.java | 76 +++++ .../io/micronaut/guides/GuidesPluginTest.java | 78 ++++++ .../micronaut/guides/IndexGeneratorTest.java | 112 ++++++++ .../io/micronaut/guides/core/AppTest.java | 2 +- .../core/DependencyMacroSubstitutionTest.java | 11 +- .../guides/core/FilesTransferUtilityTest.java | 23 ++ .../guides/core/GuideGenerationUtilsTest.java | 33 +++ .../guides/core/GuideParserTest.java | 2 + .../core/GuideProjectGeneratorTest.java | 26 ++ .../guides/core/GuideProjectZipperTest.java | 3 +- .../io/micronaut/guides/core/GuideTest.java | 18 +- .../micronaut/guides/core/GuideUtilsTest.java | 39 +++ .../micronaut/guides/core/MacroUtilsTest.java | 5 + .../core/SourceMacroSubstitutionTest.java | 14 + .../core/TestMacroSubstitutionTest.java | 13 + .../guides/core/TestScriptGeneratorTest.java | 77 ++++- .../tasks/PythonTestScriptTaskTest.java | 28 ++ .../SampleProjectGenerationTaskTest.java | 140 ++++++++++ .../test/resources/expected_test_script.sh | 16 -- .../resources/expected_test_script_native.sh | 16 -- .../python-application-config/metadata.json | 10 + .../python/config/application.toml | 2 + .../python/tests-config/application-test.toml | 2 + .../src/main/resources/bootstrap.properties | 1 + .../test/resources/bootstrap-test.properties | 1 + gradle/local-composite-builds.gradle | 151 ++++++++++ settings.gradle | 2 + ...mmon-enable-distributed-configuration.adoc | 20 +- 60 files changed, 2321 insertions(+), 179 deletions(-) create mode 100644 .github/workflows/python-guides.yml create mode 100644 buildSrc/settings.gradle create mode 100644 buildSrc/src/main/groovy/io/micronaut/guides/tasks/PythonTestScriptTask.groovy create mode 100644 buildSrc/src/main/java/io/micronaut/guides/feature/H2Runtime.java create mode 100644 buildSrc/src/main/java/io/micronaut/guides/feature/SpringSecurityCrypto.java create mode 100644 buildSrc/src/main/resources/pyronaut-test-functions.sh create mode 100644 buildSrc/src/test/java/io/micronaut/guides/DocsOnlyGuideTest.java create mode 100644 buildSrc/src/test/java/io/micronaut/guides/GuideProjectGeneratorTest.java create mode 100644 buildSrc/src/test/java/io/micronaut/guides/GuidesPluginTest.java create mode 100644 buildSrc/src/test/java/io/micronaut/guides/tasks/PythonTestScriptTaskTest.java create mode 100644 buildSrc/src/test/java/io/micronaut/guides/tasks/SampleProjectGenerationTaskTest.java create mode 100644 buildSrc/src/test/resources/file-transfer/python-application-config/metadata.json create mode 100644 buildSrc/src/test/resources/file-transfer/python-application-config/python/config/application.toml create mode 100644 buildSrc/src/test/resources/file-transfer/python-application-config/python/tests-config/application-test.toml create mode 100644 buildSrc/src/test/resources/file-transfer/python-application-config/src/main/resources/bootstrap.properties create mode 100644 buildSrc/src/test/resources/file-transfer/python-application-config/src/test/resources/bootstrap-test.properties create mode 100644 gradle/local-composite-builds.gradle diff --git a/.github/workflows/python-guides.yml b/.github/workflows/python-guides.yml new file mode 100644 index 00000000000..e8710469b46 --- /dev/null +++ b/.github/workflows/python-guides.yml @@ -0,0 +1,49 @@ +name: Validate Python Guides +on: + push: + branches: + - python + paths: + - '.github/workflows/python-guides.yml' + - 'buildSrc/**' + - 'guides/**' + - 'settings.gradle' + - 'src/docs/common/**' + pull_request: + branches: + - python + paths: + - '.github/workflows/python-guides.yml' + - 'buildSrc/**' + - 'guides/**' + - 'settings.gradle' + - 'src/docs/common/**' + +jobs: + validate: + name: Generate Python Guides + runs-on: ubuntu-latest + env: + LOCAL_GIT_MICRONAUT_STARTER: ${{ github.workspace }}/micronaut-starter + steps: + - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.2 + with: + repository: n0tl3ss/micronaut-starter + # Pin micronaut-starter#3136 until its fix is merged upstream. + ref: 763920aaa46c06aeebbaba995a66fc3bef643801 + path: micronaut-starter + - uses: actions/setup-java@v5.2.0 + with: + distribution: adopt + java-version: '25' + - uses: actions/cache@v5.0.5 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Validate build infrastructure + run: ./gradlew buildSrc:test --stacktrace + - name: Generate Python guides + run: ./gradlew generateAllPythonGuideTestScripts --stacktrace diff --git a/README.md b/README.md index 910134b540d..47611fd67f2 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,22 @@ To build a single guide, run the dynamic task created by `GuidesPlugin`; convert ./gradlew micronautHttpClientBuild ``` +### Use local composite builds + +Local Micronaut Core, Pyronaut, and Starter checkouts can be included with +either the matching Gradle property or environment variable: + +| Checkout | Gradle property | Environment variable | +| --- | --- | --- | +| Micronaut Core | `local.git.micronaut-core` | `LOCAL_GIT_MICRONAUT_CORE` | +| Pyronaut | `local.git.pyronaut` | `LOCAL_GIT_PYRONAUT` | +| Micronaut Starter | `local.git.micronaut-starter` | `LOCAL_GIT_MICRONAUT_STARTER` | + +For example: `./gradlew -Plocal.git.pyronaut=/path/to/pyronaut help`. +Gradle properties take precedence when both forms are set. Pyronaut's GraalPy +bundle repository is discovered from a local Core or Pyronaut checkout when +available; set `micronaut.graalpy.bundle.repo` to override it. + ## Create a new guide For a high-level overview of the guides infrastructure, take a look at this [blog post](https://micronaut.io/2021/04/12/improving-the-micronaut-guides-infrastructure/). diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 44e0ea18e78..ecb12e9e102 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -36,7 +36,7 @@ repositories { } ext { - micronautStarterVersion = "5.2.0" + micronautStarterVersion = "5.2.0-SNAPSHOT" } dependencies { @@ -83,5 +83,5 @@ tasks.named('test', Test) { } tasks.withType(JavaCompile).configureEach { - options.compilerArgs.add("-Amicronaut.jsonschema.baseUri=https://micronaut-projects.github.io/micronaut-guides/schemas") // (1) + options.compilerArgs.add("-Amicronaut.jsonschema.baseUri=https://guides.micronaut.io/schemas") // (1) } diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 00000000000..bf1a7b02461 --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +rootProject.name = "micronaut-guides-buildsrc" + +apply from: file('../gradle/local-composite-builds.gradle') diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy index 4284a174924..05d5387eae4 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy @@ -20,13 +20,15 @@ import org.gradle.api.GradleException import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.io.IOException import java.nio.file.Files import java.nio.file.FileVisitResult import java.nio.file.Path import java.nio.file.Paths import java.nio.file.SimpleFileVisitor -import java.time.LocalDate import java.nio.file.attribute.BasicFileAttributes +import java.time.LocalDate +import java.util.Locale import java.util.regex.Pattern import static groovy.io.FileType.FILES @@ -166,13 +168,17 @@ class GuideProjectGenerator implements AutoCloseable { copyGuideSourceFiles(inputDir, destinationPath, appName, guidesOption.language.toString()) + if (buildTool == BuildTool.PYRONAUT && lang == PYTHON) { + ensurePyronautBlockingExecutorConfig(destinationPath.resolve('config/application.toml')) + } + if (app.excludeSource()) { for (String mainSource : app.excludeSource()) { - File f = fileToDelete(destination, GuideAsciidocGenerator.mainPath(appName, mainSource), guidesOption) + File f = fileToDelete(destination, GuideAsciidocGenerator.mainPath(appName, mainSource, guidesOption), guidesOption) if (f.exists()) { f.delete() } - f = fileToDelete(destination, GuideAsciidocGenerator.mainPath(EMPTY_STRING, mainSource), guidesOption) + f = fileToDelete(destination, GuideAsciidocGenerator.mainPath(EMPTY_STRING, mainSource, guidesOption), guidesOption) if (f.exists()) { f.delete() } @@ -181,14 +187,20 @@ class GuideProjectGenerator implements AutoCloseable { if (app.excludeTest()) { for (String testSource : app.excludeTest()) { - File f = fileToDelete(destination, GuideAsciidocGenerator.testPath(appName, testSource, testFramework), guidesOption) + File f = fileToDelete(destination, GuideAsciidocGenerator.testPath(appName, testSource, guidesOption), guidesOption) if (f.exists()) { f.delete() } - f = fileToDelete(destination, GuideAsciidocGenerator.testPath(EMPTY_STRING, testSource, testFramework), guidesOption) + f = fileToDelete(destination, GuideAsciidocGenerator.testPath(EMPTY_STRING, testSource, guidesOption), guidesOption) if (f.exists()) { f.delete() } + if (buildTool == BuildTool.PYRONAUT && lang == PYTHON) { + f = new File(destination, "tests/${pythonTestModuleName(testSource)}.${lang.extension}") + if (f.exists()) { + f.delete() + } + } } } @@ -198,11 +210,41 @@ class GuideProjectGenerator implements AutoCloseable { copyFile(inputDir, destinationRoot, zipInclude) } } + if (buildTool == BuildTool.PYRONAUT && lang == PYTHON) { + removePythonPackageMarkerFiles(destination) + } addLicenses(new File(outputDir.absolutePath, folder)) } } } + private static void removePythonPackageMarkerFiles(File destination) { + Files.walkFileTree(destination.toPath(), new SimpleFileVisitor() { + @Override + FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (attrs.isRegularFile() && file.fileName.toString() == '__init__.py') { + Files.delete(file) + } + FileVisitResult.CONTINUE + } + }) + } + + private static String pythonModuleName(String target) { + if (target.contains('_') || target == target.toLowerCase(Locale.ENGLISH)) { + return target + } + target.replaceAll(/([a-z0-9])([A-Z])/, '$1_$2') + .replaceAll(/([A-Z]+)([A-Z][a-z])/, '$1_$2') + .toLowerCase(Locale.ENGLISH) + } + + private static String pythonTestModuleName(String target) { + String normalized = target.endsWith('Test') ? target.substring(0, target.length() - 'Test'.length()) : target + normalized = pythonModuleName(normalized) + normalized.startsWith('test_') ? normalized : "test_${normalized}" + } + private static void movePythonResources(Path destinationPath) { movePythonResourceFolder(destinationPath, 'views') movePythonResourceFolder(destinationPath, 'static') @@ -239,14 +281,18 @@ class GuideProjectGenerator implements AutoCloseable { void addLicenses(File folder) { String licenseHeader = licenseHeaderText() - folder.eachFileRecurse (FILES) { file -> - if ( - (file.path.endsWith(EXTENSION_JAVA) || file.path.endsWith(EXTENSION_GROOVY) || file.path.endsWith(EXTENSION_KT)) - && !file.text.contains("Licensed under") - ) { - file.text = licenseHeader + file.text + Files.walkFileTree(folder.toPath(), new SimpleFileVisitor() { + @Override + FileVisitResult visitFile(Path path, BasicFileAttributes attrs) { + File file = path.toFile() + if (attrs.isRegularFile() + && (file.path.endsWith(EXTENSION_JAVA) || file.path.endsWith(EXTENSION_GROOVY) || file.path.endsWith(EXTENSION_KT)) + && !file.text.contains("Licensed under")) { + file.text = licenseHeader + file.text + } + FileVisitResult.CONTINUE } - } + }) } @Memoized @@ -263,27 +309,20 @@ class GuideProjectGenerator implements AutoCloseable { // look for a common 'src' directory shared by multiple languages and copy those files first final String srcFolder = 'src' Path srcPath = Paths.get(inputDir.absolutePath, appName, srcFolder) + Path sourcePath = Paths.get(inputDir.absolutePath, appName, language) if (Files.exists(srcPath)) { - Path resourcesPath = srcPath.resolve('main').resolve('resources') - if (language == PYTHON.toString() && Files.exists(resourcesPath)) { - Path configPath = Paths.get(destinationPath.toString(), 'config') - Files.createDirectories(configPath) - Files.walkFileTree(resourcesPath, new CopyFileVisitor(configPath)) - Path destinationResourcesPath = Paths.get(destinationPath.toString(), srcFolder, 'main', 'resources') - Files.walkFileTree(resourcesPath, new SimpleFileVisitor() { - @Override - FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - Files.deleteIfExists(destinationResourcesPath.resolve(resourcesPath.relativize(file))) - FileVisitResult.CONTINUE - } - }) + if (language == PYTHON.toString()) { + copySharedPythonResources(srcPath, sourcePath, destinationPath) + Path pythonResources = srcPath.resolve('main').resolve('resources') + Path pythonTestResources = srcPath.resolve('test').resolve('resources') + Path pythonTestResourcesLegacy = srcPath.resolve('test-resources') Files.walkFileTree(srcPath, new CopyFileVisitor(Paths.get(destinationPath.toString(), srcFolder)) { @Override FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - if (dir.equals(resourcesPath)) { + if (dir.equals(pythonResources) || dir.equals(pythonTestResources) || dir.equals(pythonTestResourcesLegacy)) { return FileVisitResult.SKIP_SUBTREE } - super.preVisitDirectory(dir, attrs) + return super.preVisitDirectory(dir, attrs) } }) } else { @@ -291,7 +330,6 @@ class GuideProjectGenerator implements AutoCloseable { } } - Path sourcePath = Paths.get(inputDir.absolutePath, appName, language) if (!Files.exists(sourcePath)) { sourcePath.toFile().mkdir() } @@ -303,6 +341,80 @@ class GuideProjectGenerator implements AutoCloseable { } } + private static void ensurePyronautBlockingExecutorConfig(Path applicationConfig) { + String config = Files.exists(applicationConfig) ? Files.readString(applicationConfig) : '' + if (config.contains('[micronaut.executors.blocking]')) { + return + } + String suffix = config.isEmpty() || config.endsWith('\n') ? '' : '\n' + Files.createDirectories(applicationConfig.parent) + Files.writeString(applicationConfig, config + suffix + ''' +[micronaut.executors.blocking] +type = 'CACHED' +virtual = false +''') + } + + private static void copySharedPythonResources(Path srcPath, Path sourcePath, Path destinationPath) { + Path pythonConfigPath = sourcePath.resolve('config') + copySharedPythonResourceDirectory(srcPath.resolve('main/resources'), destinationPath.resolve('config'), pythonConfigPath) + Path pythonTestConfigPath = sourcePath.resolve('tests-config') + copySharedPythonResourceDirectory(srcPath.resolve('test/resources'), destinationPath.resolve('tests-config'), pythonTestConfigPath) + copySharedPythonResourceDirectory(srcPath.resolve('test-resources'), destinationPath.resolve('tests-config'), pythonTestConfigPath) + } + + private static void copySharedPythonResourceDirectory(Path resourcePath, Path destinationPath, Path pythonSpecificPath) { + if (!Files.exists(resourcePath)) { + return + } + def paths = Files.walk(resourcePath) + try { + paths.filter(Files::isRegularFile).forEach { Path source -> + Path relative = resourcePath.relativize(source) + if (!GuideUtils.isBootstrapConfiguration(relative) && !isOverriddenByPythonConfig(relative, pythonSpecificPath)) { + Path destination = destinationPath.resolve(relative) + Files.createDirectories(destination.parent) + Files.copy(source, destination, REPLACE_EXISTING) + } + } + } finally { + paths.close() + } + } + + private static boolean isOverriddenByPythonConfig(Path relative, Path pythonSpecificPath) { + if (Files.exists(pythonSpecificPath.resolve(relative))) { + return true + } + if (relative.nameCount != 1 || !Files.isDirectory(pythonSpecificPath)) { + return false + } + String filename = relative.fileName.toString() + int extensionIndex = filename.lastIndexOf('.') + if (extensionIndex < 1) { + return false + } + String basename = filename.substring(0, extensionIndex) + if (!isEnvironmentConfigBasename(basename)) { + return false + } + def paths = Files.list(pythonSpecificPath) + try { + paths.filter(Files::isRegularFile) + .map { Path path -> path.fileName.toString() } + .anyMatch { String name -> name.startsWith("${basename}.") } + } finally { + paths.close() + } + } + + private static boolean isEnvironmentConfigBasename(String basename) { + basename == 'application' || + basename.startsWith('application-') || + basename == 'bootstrap' || + basename.startsWith('bootstrap-') + } + private static File fileToDelete(File destination, String path, GuidesOption guidesOption) { Paths.get(destination.absolutePath, path .replace("@lang@", guidesOption.language.toString()) diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/GuidesPlugin.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/GuidesPlugin.groovy index 1bd550766a8..657d8609227 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/GuidesPlugin.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/GuidesPlugin.groovy @@ -15,14 +15,17 @@ import io.micronaut.guides.tasks.TestScriptRunnerTask import io.micronaut.guides.tasks.TestScriptTask import io.micronaut.guides.tasks.NativeTestScriptRunnerTask import io.micronaut.guides.tasks.NativeTestScriptTask +import io.micronaut.guides.tasks.PythonTestScriptTask import io.micronaut.json.JsonMapper import io.micronaut.starter.options.Language import org.apache.tools.ant.filters.ReplaceTokens +import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.Transformer import org.gradle.api.file.Directory +import org.gradle.api.initialization.IncludedBuild import org.gradle.api.provider.Provider import org.gradle.api.tasks.Copy import org.gradle.api.tasks.TaskProvider @@ -36,6 +39,7 @@ import java.util.stream.Collectors import static io.micronaut.guides.GuideProjectGenerator.DEFAULT_APP_NAME import static io.micronaut.starter.options.BuildTool.MAVEN +import static io.micronaut.starter.options.BuildTool.PYRONAUT @CompileStatic class GuidesPlugin implements Plugin { @@ -50,17 +54,55 @@ class GuidesPlugin implements Plugin { private static final String KEY_WORKFLOW = "workflow" private static final String KEY_WORKFLOW_SNAPSHOT = "workflow-snapshot" private static final String TEST_RUNNER = "test-runner" + private static final String PYTHON_TEST_SCRIPT = "python-test-script" private static final String PYTHON_TEST_RUNNER = "python-test-runner" private static final String KEY_DOC = "doc" private static final String KEY_PYTHON_BUILD = "python-build" private static final String COMMA = "," private static final String TASK_SUFFIX_BUILD = "Build" + private static final String LOCAL_GIT_PYRONAUT_PROPERTY = "local.git.pyronaut" + private static final String LOCAL_GIT_PYRONAUT_ENV = "LOCAL_GIT_PYRONAUT" + private static final String LOCAL_PYRONAUT_CORE_VERSION_PROPERTY = "local.pyronaut.core.version" + private static final String LOCAL_PYRONAUT_CORE_VERSION_ENV = "LOCAL_PYRONAUT_CORE_VERSION" + private static final String LOCAL_PYRONAUT_PLATFORM_VERSION_PROPERTY = "local.pyronaut.platform.version" + private static final String LOCAL_PYRONAUT_PLATFORM_VERSION_ENV = "LOCAL_PYRONAUT_PLATFORM_VERSION" + private static final String DEFAULT_LOCAL_PYRONAUT_CORE_VERSION = "5.2.3" + private static final String DEFAULT_LOCAL_PYRONAUT_PLATFORM_VERSION = "5.1.0" + private static final String PYRONAUT_INCLUDED_BUILD_NAME = "pyronaut" + private static final String PYRONAUT_FIXTURE_REPOSITORY = "functional-test/build/fixture-repo" + private static final String PYRONAUT_INSTALL_EXECUTABLE = "pyronaut-install/build/install/micronaut-pyronaut-install/bin/pyronaut-install" + private static final String PYRONAUT_VALIDATE_CONFIG_EXECUTABLE = "pyronaut-validate-config/build/install/micronaut-pyronaut-validate-config/bin/pyronaut-validate-config" + private static final String PYRONAUT_PROCESS_EXECUTABLE = "pyronaut-processor/build/install/micronaut-pyronaut-processor/bin/pyronaut-processor" + private static final String PYRONAUT_TEST_EXECUTABLE = "pyronaut-test/build/install/micronaut-pyronaut-test/bin/pyronaut-test" + private static final String PYRONAUT_TEST_RESOURCES_SERVER_EXECUTABLE = "pyronaut-test-resources-server/build/install/micronaut-pyronaut-test-resources-server/bin/pyronaut-test-resources-server" + private static final String PYRONAUT_CLI_PYTHONPATH = "pyronaut/src/main/python" + private static final String PYRONAUT_FIXTURE_LAUNCHER_TASK = ":micronaut-functional-test:installFixtureLaunchers" + private static final List PYRONAUT_FIXTURE_STAGE_TASKS = List.of( + ":micronaut-functional-test:stagePyronautFixtureArtifacts", + ":micronaut-functional-test:stageMicronautPlatformFixtureArtifact", + ":micronaut-functional-test:stageMicronautCoreFixtureArtifacts", + ":micronaut-functional-test:stageMicronautDataFixtureArtifacts", + ":micronaut-functional-test:stageSourcegenFixtureArtifacts", + ":micronaut-functional-test:stageIncludedCoreExternalFixtureArtifacts", + ":micronaut-functional-test:stageMicronautTestFixtureArtifacts" + ) @Override void apply(Project project) { GuideProjectGenerator projectGenerator = new GuideProjectGenerator() Directory guidesDir = project.layout.projectDirectory.dir("guides") Provider codeDir = project.layout.buildDirectory.dir("code") + Provider localPyronautPath = localGitPath(project, LOCAL_GIT_PYRONAUT_PROPERTY, LOCAL_GIT_PYRONAUT_ENV) + Provider localPyronautCoreVersion = configuredValue(project, LOCAL_PYRONAUT_CORE_VERSION_PROPERTY, LOCAL_PYRONAUT_CORE_VERSION_ENV, DEFAULT_LOCAL_PYRONAUT_CORE_VERSION) + Provider localPyronautPlatformVersion = configuredValue(project, LOCAL_PYRONAUT_PLATFORM_VERSION_PROPERTY, LOCAL_PYRONAUT_PLATFORM_VERSION_ENV, DEFAULT_LOCAL_PYRONAUT_PLATFORM_VERSION) + Provider localPyronautRepository = localPyronautPath.map(path -> new File(path, PYRONAUT_FIXTURE_REPOSITORY).absolutePath) + Provider localPyronautInstallExecutable = localPyronautPath.map(path -> new File(path, PYRONAUT_INSTALL_EXECUTABLE).absolutePath) + Provider localPyronautValidateConfigExecutable = localPyronautPath.map(path -> new File(path, PYRONAUT_VALIDATE_CONFIG_EXECUTABLE).absolutePath) + Provider localPyronautProcessExecutable = localPyronautPath.map(path -> new File(path, PYRONAUT_PROCESS_EXECUTABLE).absolutePath) + Provider localPyronautTestExecutable = localPyronautPath.map(path -> new File(path, PYRONAUT_TEST_EXECUTABLE).absolutePath) + Provider localPyronautTestResourcesServerExecutable = localPyronautPath.map(path -> new File(path, PYRONAUT_TEST_RESOURCES_SERVER_EXECUTABLE).absolutePath) + Provider localPyronautCliPythonPath = localPyronautPath.map(path -> new File(path, PYRONAUT_CLI_PYTHONPATH).absolutePath) + TaskProvider stageLocalPyronautArtifactsTask = registerStageLocalPyronautArtifactsTask(project, localPyronautRepository) Properties testProps = guidesDir.file("tests.properties").asFile.withInputStream { inputStream -> new Properties().tap { load(inputStream) @@ -78,32 +120,34 @@ class GuidesPlugin implements Plugin { .filter(guideMetadata -> Utils.process(guideMetadata, false)) .map(metadata -> { String taskSlug = kebabCaseToGradleName(metadata.slug()) + boolean hasApps = !metadata.apps().isEmpty() - TaskProvider githubActionWorkflowTask = registerGenerateGithubActionWorkflow(project, + TaskProvider githubActionWorkflowTask = hasApps ? registerGenerateGithubActionWorkflow(project, metadata, - taskSlug) + taskSlug) : null - TaskProvider githubActionSnapshotWorkflowTask = registerGenerateGithubActionSnapshotWorkflow(project, + TaskProvider githubActionSnapshotWorkflowTask = hasApps ? registerGenerateGithubActionSnapshotWorkflow(project, metadata, - taskSlug) + taskSlug) : null List options = GuideProjectGenerator.guidesOptions(metadata) TaskProvider generateTask = registerGenerateTask(project, metadata, projectGenerator, guidesDir, codeDir, taskSlug) TaskProvider docTask = registerDocTask(project, metadata, guidesDir, generateTask, taskSlug) - List> zippers = options.stream() + List> zippers = hasApps ? options.stream() .map(option -> { registerZipTask(project, metadata, option, generateTask) - }).collect(Collectors.toList()) - TaskProvider zip = registerZipTask(project, taskSlug, metadata, zippers) + }).collect(Collectors.toList()) : [] + TaskProvider zip = hasApps ? registerZipTask(project, taskSlug, metadata, zippers) : null TaskProvider indexTask = registerIndexTask(project, taskSlug, metadata) - TaskProvider testScriptTask = registerTestScriptTask(project, taskSlug, metadata, generateTask) - TaskProvider testScriptRunnerTask = registerTestScriptRunnerTask(project, taskSlug, metadata, testScriptTask) - TaskProvider nativeTestScriptTask = registerNativeTestScriptTask(project, taskSlug, metadata, generateTask) - TaskProvider nativeTestScriptRunnerTask = registerNativeTestScriptRunnerTask(project, taskSlug, metadata, nativeTestScriptTask) + TaskProvider testScriptTask = hasApps ? registerTestScriptTask(project, taskSlug, metadata, generateTask) : null + TaskProvider testScriptRunnerTask = hasApps ? registerTestScriptRunnerTask(project, taskSlug, metadata, testScriptTask) : null + TaskProvider nativeTestScriptTask = hasApps ? registerNativeTestScriptTask(project, taskSlug, metadata, generateTask) : null + TaskProvider nativeTestScriptRunnerTask = hasApps ? registerNativeTestScriptRunnerTask(project, taskSlug, metadata, nativeTestScriptTask) : null - registerGuideBuild(project, taskSlug, metadata, docTask, zip, indexTask, testScriptTask, testScriptRunnerTask, nativeTestScriptTask, nativeTestScriptRunnerTask) TaskProvider pythonBuildTask = null TaskProvider pythonTestRunnerTask = null + TaskProvider pythonTestScriptTask = null + List> guideBuildTasks = [docTask, zip, indexTask, testScriptTask, testScriptRunnerTask, nativeTestScriptTask, nativeTestScriptRunnerTask].findAll { it != null } List languages = options.stream() .map(option -> option.language) .distinct() @@ -111,21 +155,59 @@ class GuidesPlugin implements Plugin { for (Language language : languages) { GuidesOption languageOption = options.find { GuidesOption option -> option.language == language } TaskProvider languageGenerateTask = registerGenerateTask(project, metadata, projectGenerator, guidesDir, codeDir, taskSlug, language) + languageGenerateTask.configure { it.mustRunAfter(generateTask) } TaskProvider languageDocTask = registerDocTask(project, metadata, guidesDir, languageGenerateTask, taskSlug, language) + if (!hasApps) { + TaskProvider languageBuildTask = registerGuideBuildForLanguage(project, taskSlug, language.toString().capitalize(), metadata, languageDocTask) + if (language == Language.PYTHON) { + pythonBuildTask = languageBuildTask + } + continue + } TaskProvider languageZipTask = registerLanguageZipTask(project, taskSlug, metadata, language, languageOption, languageGenerateTask) - TaskProvider languageTestScriptTask = registerTestScriptTask(project, taskSlug, metadata, languageGenerateTask, language) - TaskProvider languageTestScriptRunnerTask = registerTestScriptRunnerTask(project, taskSlug, metadata, languageTestScriptTask, language) - TaskProvider languageBuildTask = registerGuideBuildForLanguage(project, taskSlug, language.toString().capitalize(), metadata, languageDocTask, languageZipTask, languageTestScriptTask, languageTestScriptRunnerTask) if (language == Language.PYTHON) { - pythonBuildTask = languageBuildTask - pythonTestRunnerTask = languageTestScriptRunnerTask as TaskProvider + for (int i = 0; i < options.size(); i++) { + if (options.get(i).language == Language.PYTHON) { + zippers.get(i).configure { it.dependsOn(languageGenerateTask) } + } + } + TaskProvider pythonScript = registerPythonTestScriptTask(project, taskSlug, metadata, languageGenerateTask) + TaskProvider pythonRunner = registerPythonTestScriptRunnerTask( + project, + taskSlug, + metadata, + pythonScript, + stageLocalPyronautArtifactsTask, + localPyronautRepository, + localPyronautCoreVersion, + localPyronautPlatformVersion, + localPyronautInstallExecutable, + localPyronautValidateConfigExecutable, + localPyronautProcessExecutable, + localPyronautTestExecutable, + localPyronautTestResourcesServerExecutable, + localPyronautCliPythonPath + ) + pythonBuildTask = registerGuideBuildForLanguage(project, taskSlug, language.toString().capitalize(), metadata, languageDocTask, languageZipTask, pythonScript, pythonRunner) + pythonTestRunnerTask = pythonRunner as TaskProvider + pythonTestScriptTask = pythonScript as TaskProvider + guideBuildTasks.add(pythonScript) + guideBuildTasks.add(pythonRunner) + } else { + TaskProvider languageTestScriptTask = registerTestScriptTask(project, taskSlug, metadata, languageGenerateTask, language) + TaskProvider languageTestScriptRunnerTask = registerTestScriptRunnerTask(project, taskSlug, metadata, languageTestScriptTask, language) + registerGuideBuildForLanguage(project, taskSlug, language.toString().capitalize(), metadata, languageDocTask, languageZipTask, languageTestScriptTask, languageTestScriptRunnerTask) } } + registerGuideBuild(project, taskSlug, metadata, guideBuildTasks) Map> taskMap = [(KEY_DOC) : docTask, (KEY_ZIP) : zip, (KEY_WORKFLOW) : githubActionWorkflowTask, (KEY_WORKFLOW_SNAPSHOT): githubActionSnapshotWorkflowTask, (TEST_RUNNER) : testScriptRunnerTask] as Map> + if (pythonTestScriptTask != null) { + taskMap.put(PYTHON_TEST_SCRIPT, pythonTestScriptTask) + } if (pythonBuildTask != null) { taskMap.put(KEY_PYTHON_BUILD, pythonBuildTask) } @@ -139,6 +221,9 @@ class GuidesPlugin implements Plugin { .map(m -> m.get(KEY_DOC)) .toList() as List> + project.tasks.named("asciidoctor").configure { Task it -> + it.mustRunAfter(project.tasks.withType(AsciidocGenerationTask)) + } List> pythonBuildTasks = sampleTasks.stream() .map(m -> m.get(KEY_PYTHON_BUILD)) .filter(task -> task != null) @@ -164,14 +249,14 @@ class GuidesPlugin implements Plugin { project.tasks.register("testsGroup${i + 1}") { Task it -> it.group = 'guides' it.description = "Run group of guide tests" - it.dependsOn(tasks.collect { it[TEST_RUNNER] }) + it.dependsOn(tasks.collect { it[TEST_RUNNER] }.findAll { it != null }) } } project.tasks.register("runAllGuideTests") { Task it -> it.group = 'guides' it.description = 'Runs all Guide test scripts' - it.dependsOn(sampleTasks.stream().map(m -> m.get(TEST_RUNNER)).collect(Collectors.toList())) + it.dependsOn(sampleTasks.stream().map(m -> m.get(TEST_RUNNER)).filter(task -> task != null).collect(Collectors.toList())) } List> pythonTestRunnerTasks = sampleTasks.stream() @@ -179,14 +264,37 @@ class GuidesPlugin implements Plugin { .filter(task -> task != null) .toList() as List> - project.tasks.register("runAllPythonGuidesTests") { Task it -> + if (!pythonTestRunnerTasks.isEmpty()) { + int pythonGroupSize = (pythonTestRunnerTasks.size() / Integer.parseInt(testProps.get("numberOfTestGroups") as String)) + .setScale(0, RoundingMode.UP).toInteger() + pythonTestRunnerTasks.collate(pythonGroupSize, true).eachWithIndex { List> tasks, int i -> + project.tasks.register("pythonTestsGroup${i + 1}") { Task it -> + it.group = 'guides' + it.description = "Run group of Python guide tests" + it.dependsOn(tasks) + } + } + } + + project.tasks.register("runAllPythonGuideTests") { Task it -> it.group = 'guides' it.description = 'Runs all Python Guide test scripts' it.dependsOn(pythonTestRunnerTasks) } + List> pythonTestScriptTasks = sampleTasks.stream() + .map(m -> m.get(PYTHON_TEST_SCRIPT)) + .filter(task -> task != null) + .toList() as List> + + project.tasks.register("generateAllPythonGuideTestScripts") { Task it -> + it.group = 'guides' + it.description = 'Generates every Python guide project and test script without running Pyronaut' + it.dependsOn(pythonTestScriptTasks) + } List> zipTasks = sampleTasks.stream() .map(m -> m.get(KEY_ZIP)) + .filter(task -> task != null) .toList() as List> project.tasks.register("generateCodeZip") { Task it -> @@ -197,9 +305,11 @@ class GuidesPlugin implements Plugin { List> workflowTasks = sampleTasks.stream() .map(m -> m.get(KEY_WORKFLOW)) + .filter(task -> task != null) .toList() as List> workflowTasks.addAll(sampleTasks.stream() .map(m -> m.get(KEY_WORKFLOW_SNAPSHOT)) + .filter(task -> task != null) .toList()) project.tasks.register("generateGithubActionWorkflows") { Task it -> @@ -209,6 +319,42 @@ class GuidesPlugin implements Plugin { } } + private static Provider localGitPath(Project project, + String propertyName, + String environmentName) { + project.providers.gradleProperty(propertyName) + .orElse(project.providers.environmentVariable(environmentName)) + } + + private static Provider configuredValue(Project project, + String propertyName, + String environmentName, + String defaultValue) { + project.providers.gradleProperty(propertyName) + .orElse(project.providers.environmentVariable(environmentName)) + .orElse(defaultValue) + } + + private static TaskProvider registerStageLocalPyronautArtifactsTask(Project project, + Provider localPyronautRepository) { + IncludedBuild includedBuild = project.gradle.includedBuilds.find { IncludedBuild build -> + build.name == PYRONAUT_INCLUDED_BUILD_NAME + } + project.tasks.register("stageLocalPyronautArtifacts") { Task it -> + it.group = "build setup" + it.description = "Stages Pyronaut artifacts from the local included Pyronaut checkout into its fixture repository." + it.outputs.dir(localPyronautRepository.map(path -> new File(path))) + if (includedBuild != null) { + it.dependsOn(PYRONAUT_FIXTURE_STAGE_TASKS.collect { String taskPath -> includedBuild.task(taskPath) }) + it.dependsOn(includedBuild.task(PYRONAUT_FIXTURE_LAUNCHER_TASK)) + } else { + it.doFirst { + throw new GradleException("Python guide tests require an included '${PYRONAUT_INCLUDED_BUILD_NAME}' build. Configure ${LOCAL_GIT_PYRONAUT_PROPERTY} or ${LOCAL_GIT_PYRONAUT_ENV}.") + } + } + } + } + /** * https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore */ @@ -279,7 +425,8 @@ class GuidesPlugin implements Plugin { it.metadata = metadata it.guideSlug.set(metadata.slug()) it.metadataFile.set(project.layout.projectDirectory.dir("guides/${metadata.slug()}").file("metadata.json")) - it.scriptFile.set(project.layout.buildDirectory.dir("code/${metadata.slug()}").map(d -> d.file("test.sh"))) + String scriptFileName = language ? "test-${language.toString().toLowerCase()}.sh" : "test.sh" + it.scriptFile.set(project.layout.buildDirectory.dir("code/${metadata.slug()}").map(d -> d.file(scriptFileName))) if (language) { it.language.set(language.name()) } @@ -287,6 +434,22 @@ class GuidesPlugin implements Plugin { } } + private static TaskProvider registerPythonTestScriptTask(Project project, + String taskSlug, + Guide metadata, + TaskProvider generateTask) { + project.tasks.register("${taskSlug}PythonTestScript", PythonTestScriptTask) { PythonTestScriptTask it -> + it.group = "guides ${metadata.slug()}" + it.description = "Create a python-test.sh script for the Pyronaut project generated by ${metadata.slug()}" + it.metadata = metadata + it.guideSlug.set(metadata.slug()) + it.metadataFile.set(project.layout.projectDirectory.dir("guides/${metadata.slug()}").file("metadata.json")) + it.pyronautTestFunctionsFile.set(project.layout.projectDirectory.file("buildSrc/src/main/resources/pyronaut-test-functions.sh")) + it.scriptFile.set(project.layout.buildDirectory.dir("code/${metadata.slug()}").map(d -> d.file("python-test.sh"))) + it.dependsOn(generateTask) + } + } + private static TaskProvider registerNativeTestScriptTask(Project project, String taskSlug, Guide metadata, @@ -359,6 +522,46 @@ class GuidesPlugin implements Plugin { } } + private static TaskProvider registerPythonTestScriptRunnerTask(Project project, + String taskSlug, + Guide metadata, + TaskProvider pythonTestScriptTask, + TaskProvider stageLocalPyronautArtifactsTask, + Provider localPyronautRepository, + Provider localPyronautCoreVersion, + Provider localPyronautPlatformVersion, + Provider localPyronautInstallExecutable, + Provider localPyronautValidateConfigExecutable, + Provider localPyronautProcessExecutable, + Provider localPyronautTestExecutable, + Provider localPyronautTestResourcesServerExecutable, + Provider localPyronautCliPythonPath) { + project.tasks.register("${taskSlug}RunPythonTestScript", TestScriptRunnerTask) { TestScriptRunnerTask it -> + it.onlyIf { !Utils.skipBecauseOfJavaVersion(metadata) } + + Provider codeDirectory = project.layout.buildDirectory.dir("code/${metadata.slug()}") + + it.group = "guides ${metadata.slug()}" + it.description = "Run the Python tests for the Pyronaut project generated by ${metadata.slug()}" + + it.environment.set(metadata.env()) + it.environment.put("PYRONAUT_LOCAL_REPOSITORY", localPyronautRepository) + it.environment.put("PYRONAUT_LOCAL_CORE_VERSION", localPyronautCoreVersion) + it.environment.put("PYRONAUT_LOCAL_PLATFORM_VERSION", localPyronautPlatformVersion) + it.environment.put("PYRONAUT_INSTALL_EXECUTABLE", localPyronautInstallExecutable) + it.environment.put("PYRONAUT_VALIDATE_CONFIG_EXECUTABLE", localPyronautValidateConfigExecutable) + it.environment.put("PYRONAUT_PROCESS_EXECUTABLE", localPyronautProcessExecutable) + it.environment.put("PYRONAUT_PROCESSOR_EXECUTABLE", localPyronautProcessExecutable) + it.environment.put("PYRONAUT_TEST_EXECUTABLE", localPyronautTestExecutable) + it.environment.put("PYRONAUT_TEST_RESOURCES_SERVER_EXECUTABLE", localPyronautTestResourcesServerExecutable) + it.environment.put("PYRONAUT_CLI_PYTHONPATH", localPyronautCliPythonPath) + it.testScript.set(pythonTestScriptTask.flatMap { t -> t.scriptFile }) + it.guideSourceDirectory.set(project.layout.projectDirectory.dir("guides/${metadata.slug()}")) + it.outputFile.set(codeDirectory.map(d -> d.file("python-output.log"))) + it.dependsOn(stageLocalPyronautArtifactsTask) + } + } + private static TaskProvider registerDocTask(Project project, Guide metadata, Directory guidesDir, @@ -443,21 +646,30 @@ class GuidesPlugin implements Plugin { .filter(option -> option.buildTool == MAVEN) .collect(Collectors.toList()) + List pyronautOptions = options + .stream() + .filter(option -> option.buildTool == PYRONAUT) + .collect(Collectors.toList()) + String gradleProjects = projects(metadata, gradleOptions) String mavenProjects = projects(metadata, mavenOptions) + String pyronautProjects = projects(metadata, pyronautOptions) boolean mavenEnabled = !(CollectionUtils.isEmpty(mavenOptions) || metadata.skipMavenTests()) boolean gradleEnabled = !(CollectionUtils.isEmpty(gradleOptions) || metadata.skipGradleTests()) + boolean pyronautEnabled = !(CollectionUtils.isEmpty(pyronautOptions) || metadata.skipPyronautTests()) [ gradleTask : taskSlug + TASK_SUFFIX_GENERATE_PROJECTS, javaMatrix : javaMatrix(metadata), slug : metadata.slug(), mavenProjects : mavenProjects, gradleProjects: gradleProjects, + pyronautProjects: pyronautProjects, paths : workflowPaths(metadata), mavenEnabled : String.valueOf(mavenEnabled), - gradleEnabled : String.valueOf(gradleEnabled) + gradleEnabled : String.valueOf(gradleEnabled), + pyronautEnabled: String.valueOf(pyronautEnabled) ] as Map } @@ -566,6 +778,10 @@ class GuidesPlugin implements Plugin { if (language) { it.language.set(language.name()) } + List projectOutputDirectories = SampleProjectGenerationTask.outputDirectoryNames(metadata, language) + it.outputDirectories.from(projectOutputDirectories.collect { String outputDirectoryName -> + codeDir.map(outputDirectory -> outputDirectory.dir("${metadata.slug()}/${outputDirectoryName}")) + }) it.guidesGenerator = projectGenerator it.metadata = metadata } @@ -574,7 +790,7 @@ class GuidesPlugin implements Plugin { private static TaskProvider registerGuideBuild(Project project, String taskSlug, Guide metadata, - TaskProvider... dependsOnTasks) { + Collection> dependsOnTasks) { project.tasks.register("${taskSlug}${TASK_SUFFIX_BUILD}") { Task it -> it.group = "guides ${metadata.slug()}" it.dependsOn(dependsOnTasks) diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/IndexGenerator.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/IndexGenerator.groovy index 05ef442310a..dbe8a54df07 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/IndexGenerator.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/IndexGenerator.groovy @@ -456,6 +456,7 @@ class IndexGenerator { String mavenImg = 'Maven' String gradleImg = 'Gradle' String pyronautImg = 'Pyronaut' + boolean hasPython = guidesOptionList.find { GuidesOption option -> option.language == Language.PYTHON } != null String tableHtml = """\ @@ -466,7 +467,9 @@ class IndexGenerator { tableHtml += "" tableHtml += "" tableHtml += "" - tableHtml += "" + if (hasPython) { + tableHtml += "" + } tableHtml += """\ @@ -481,7 +484,9 @@ class IndexGenerator { tableHtml += cell(baseURL, metadata, BuildTool.GRADLE, Language.JAVA, guidesOptionList) tableHtml += cell(baseURL, metadata, BuildTool.GRADLE, Language.KOTLIN, guidesOptionList) tableHtml += cell(baseURL, metadata, BuildTool.GRADLE, Language.GROOVY, guidesOptionList) - tableHtml += cell(baseURL, metadata, BuildTool.GRADLE, Language.PYTHON, guidesOptionList) + if (hasPython) { + tableHtml += "" + } tableHtml += """\ @@ -495,7 +500,9 @@ class IndexGenerator { tableHtml += cell(baseURL, metadata, BuildTool.MAVEN, Language.JAVA, guidesOptionList) tableHtml += cell(baseURL, metadata, BuildTool.MAVEN, Language.KOTLIN, guidesOptionList) tableHtml += cell(baseURL, metadata, BuildTool.MAVEN, Language.GROOVY, guidesOptionList) - tableHtml += cell(baseURL, metadata, BuildTool.MAVEN, Language.PYTHON, guidesOptionList) + if (hasPython) { + tableHtml += "" + } tableHtml += """\ @@ -506,9 +513,7 @@ class IndexGenerator { """ - tableHtml += cell(baseURL, metadata, BuildTool.PYRONAUT, Language.JAVA, guidesOptionList) - tableHtml += cell(baseURL, metadata, BuildTool.PYRONAUT, Language.KOTLIN, guidesOptionList) - tableHtml += cell(baseURL, metadata, BuildTool.PYRONAUT, Language.GROOVY, guidesOptionList) + tableHtml += "" tableHtml += cell(baseURL, metadata, BuildTool.PYRONAUT, Language.PYTHON, guidesOptionList) tableHtml += """\ diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/TestScriptGenerator.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/TestScriptGenerator.groovy index 4638afedbbe..fac89293093 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/TestScriptGenerator.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/TestScriptGenerator.groovy @@ -102,7 +102,7 @@ exit 0 List metadatas, boolean stopIfFailure, Language languageFilter) { - String script = generateScript(metadatas, stopIfFailure, false, languageFilter) + String script = generateScript(metadatas, stopIfFailure, false, false, languageFilter) generateTestScript(output, script) } @@ -113,6 +113,13 @@ exit 0 generateTestScript(output, script, 'native-test.sh') } + static void generatePythonTestScript(File output, + List metadatas, + boolean stopIfFailure) { + String script = generateScript(metadatas, stopIfFailure, false, true) + generateTestScript(output, script, 'python-test.sh') + } + static void generateTestScript(File output, String script, String scriptFileName = "test.sh") { File testScript = new File(output, scriptFileName) testScript.createNewFile() @@ -139,6 +146,7 @@ exit 0 static String generateScript(List metadatas, boolean stopIfFailure, boolean nativeTest = false, + boolean pythonTest = false, Language languageFilter = null) { StringBuilder bashScript = new StringBuilder('''\ #!/usr/bin/env bash @@ -155,9 +163,15 @@ kill_kotlin_daemon () { done } ''') + if (pythonTest) { + bashScript << "\n\n" << pyronautFunctions() + } metadatas.sort { it.slug() } for (Guide metadata : metadatas) { + if (metadata.apps().isEmpty()) { + continue + } List guidesOptionList = GuideProjectGenerator.guidesOptions(metadata) if (languageFilter != null) { guidesOptionList = guidesOptionList.findAll { GuidesOption option -> option.language == languageFilter } @@ -165,6 +179,9 @@ kill_kotlin_daemon () { bashScript << """\ """ for (GuidesOption guidesOption : guidesOptionList) { + if (pythonTest != isPyronautPython(guidesOption)) { + continue + } String folder = GuideProjectGenerator.folderName(metadata.slug(), guidesOption) BuildTool buildTool = guidesOption.getBuildTool() if (buildTool == PYRONAUT && System.getenv('CI') != null) { @@ -242,9 +259,7 @@ if (noDaemon) { } if (buildTool == PYRONAUT) { bashScript += """\ -pyronaut install || EXIT_STATUS=\$? -pyronaut validate-config || EXIT_STATUS=\$? -pyronaut test || EXIT_STATUS=\$? +run_pyronaut_tests || EXIT_STATUS=\$? """ } else if (nativeTest) { bashScript += """\ @@ -283,4 +298,18 @@ EXIT_STATUS=0 bashScript } + + static String pyronautFunctions() { + java.io.InputStream stream = TestScriptGenerator.class.getResourceAsStream('/pyronaut-test-functions.sh') + if (stream == null) { + throw new IllegalStateException('Missing pyronaut-test-functions.sh resource') + } + try (stream) { + new String(stream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8).stripTrailing() + '\n' + } + } + + private static boolean isPyronautPython(GuidesOption guidesOption) { + guidesOption.buildTool == PYRONAUT && guidesOption.language == Language.PYTHON + } } diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/tasks/PythonTestScriptTask.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/tasks/PythonTestScriptTask.groovy new file mode 100644 index 00000000000..6dec74d94c5 --- /dev/null +++ b/buildSrc/src/main/groovy/io/micronaut/guides/tasks/PythonTestScriptTask.groovy @@ -0,0 +1,44 @@ +package io.micronaut.guides.tasks + +import groovy.transform.CompileStatic +import io.micronaut.guides.TestScriptGenerator +import io.micronaut.guides.core.Guide +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.TaskAction + +import static org.gradle.api.tasks.PathSensitivity.RELATIVE + +@CompileStatic +@CacheableTask +abstract class PythonTestScriptTask extends DefaultTask { + + @Input + abstract Property getGuideSlug() + + @InputFile + @PathSensitive(RELATIVE) + abstract RegularFileProperty getMetadataFile() + + @InputFile + @PathSensitive(RELATIVE) + abstract RegularFileProperty getPyronautTestFunctionsFile() + + @Internal + Guide metadata + + @OutputFile + abstract RegularFileProperty getScriptFile() + + @TaskAction + def perform() { + TestScriptGenerator.generatePythonTestScript(scriptFile.get().asFile.parentFile, [metadata], false) + } +} diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/tasks/SampleProjectGenerationTask.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/tasks/SampleProjectGenerationTask.groovy index 4d4da9e33c3..df4b3f8d1a6 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/tasks/SampleProjectGenerationTask.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/tasks/SampleProjectGenerationTask.groovy @@ -3,8 +3,10 @@ package io.micronaut.guides.tasks import groovy.transform.CompileStatic import io.micronaut.guides.GuideProjectGenerator import io.micronaut.guides.core.Guide +import io.micronaut.guides.core.GuidesOption import io.micronaut.starter.options.Language import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask @@ -12,7 +14,7 @@ import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputDirectories import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.TaskAction @@ -22,6 +24,15 @@ import static org.gradle.api.tasks.PathSensitivity.RELATIVE @CacheableTask abstract class SampleProjectGenerationTask extends DefaultTask { + static List outputDirectoryNames(Guide metadata, Language language) { + if (metadata.apps().isEmpty()) { + return [] + } + GuideProjectGenerator.guidesOptions(metadata) + .findAll { GuidesOption option -> language == null || option.language == language } + .collect { GuidesOption option -> GuideProjectGenerator.folderName(metadata.slug(), option) } + } + @Internal GuideProjectGenerator guidesGenerator @@ -44,12 +55,21 @@ abstract class SampleProjectGenerationTask extends DefaultTask { @PathSensitive(RELATIVE) abstract DirectoryProperty getBaseInputDirectory() - @OutputDirectory + @Internal abstract DirectoryProperty getOutputDir() + @OutputDirectories + abstract ConfigurableFileCollection getOutputDirectories() + @TaskAction def perform() { + File outputDirectory = outputDir.get().asFile Language languageFilter = language.isPresent() ? Language.valueOf(language.get()) : null - guidesGenerator.generateOne(metadata, inputDirectory.get().asFile, outputDir.get().asFile, languageFilter) + if (languageFilter == null) { + project.delete(outputDirectory) + } else { + project.delete(outputDirectories) + } + guidesGenerator.generateOne(metadata, inputDirectory.get().asFile, outputDirectory, languageFilter) } } diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/tasks/TestScriptRunnerWorkAction.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/tasks/TestScriptRunnerWorkAction.groovy index 95674882937..76f1adef558 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/tasks/TestScriptRunnerWorkAction.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/tasks/TestScriptRunnerWorkAction.groovy @@ -24,7 +24,7 @@ abstract class TestScriptRunnerWorkAction implements WorkAction pythonFeatures ) { + public App( + @NonNull String name, + @Nullable String packageName, + @Nullable ApplicationType applicationType, + @Nullable String framework, + @Nullable List features, + @Nullable List invisibleFeatures, + @Nullable List kotlinFeatures, + @Nullable List javaFeatures, + @Nullable List groovyFeatures, + @Nullable TestFramework testFramework, + @Nullable List excludeTest, + @Nullable List excludeSource, + @Nullable Boolean validateLicense) { + this(name, packageName, applicationType, framework, features, invisibleFeatures, kotlinFeatures, javaFeatures, + groovyFeatures, null, testFramework, excludeTest, excludeSource, validateLicense, null); + } + + public App( + @NonNull String name, + @Nullable String packageName, + @Nullable ApplicationType applicationType, + @Nullable String framework, + @Nullable List features, + @Nullable List invisibleFeatures, + @Nullable List kotlinFeatures, + @Nullable List javaFeatures, + @Nullable List groovyFeatures, + @Nullable List jvmFeatures, + @Nullable TestFramework testFramework, + @Nullable List excludeTest, + @Nullable List excludeSource, + @Nullable Boolean validateLicense) { + this(name, packageName, applicationType, framework, features, invisibleFeatures, kotlinFeatures, javaFeatures, + groovyFeatures, jvmFeatures, testFramework, excludeTest, excludeSource, validateLicense, null); + } } - diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/DefaultFilesTransferUtility.java b/buildSrc/src/main/java/io/micronaut/guides/core/DefaultFilesTransferUtility.java index 1c3fe9cf074..73bed2c2c0d 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/DefaultFilesTransferUtility.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/DefaultFilesTransferUtility.java @@ -10,6 +10,7 @@ import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -19,8 +20,11 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import static io.micronaut.core.util.StringUtils.EMPTY_STRING; +import static io.micronaut.starter.options.BuildTool.PYRONAUT; +import static io.micronaut.starter.options.Language.PYTHON; @Singleton public class DefaultFilesTransferUtility implements FilesTransferUtility { @@ -48,30 +52,23 @@ private static boolean fileContainsText(File file, String text) { } private static void copyGuideSourceFiles(File inputDir, Path destinationPath, - String appName, String language, + String appName, Language language, boolean ignoreMissingDirectories) throws IOException { // look for a common 'src' directory shared by multiple languages and copy those files first final String srcFolder = "src"; Path srcPath = Paths.get(inputDir.getAbsolutePath(), appName, srcFolder); + Path sourcePath = Paths.get(inputDir.getAbsolutePath(), appName, language.toString()); if (Files.exists(srcPath)) { - Path resourcesPath = srcPath.resolve("main").resolve("resources"); - if (Language.PYTHON.toString().equals(language) && Files.exists(resourcesPath)) { - Path configPath = Paths.get(destinationPath.toString(), "config"); - Files.createDirectories(configPath); - Files.walkFileTree(resourcesPath, new CopyFileVisitor(configPath)); - Path destinationResourcesPath = Paths.get(destinationPath.toString(), srcFolder, "main", "resources"); - Files.walkFileTree(resourcesPath, new SimpleFileVisitor<>() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.deleteIfExists(destinationResourcesPath.resolve(resourcesPath.relativize(file))); - return FileVisitResult.CONTINUE; - } - }); + if (language == PYTHON) { + copySharedPythonResources(srcPath, sourcePath, destinationPath); + Path pythonResources = srcPath.resolve("main").resolve("resources"); + Path pythonTestResources = srcPath.resolve("test").resolve("resources"); + Path pythonTestResourcesLegacy = srcPath.resolve("test-resources"); Files.walkFileTree(srcPath, new CopyFileVisitor(Paths.get(destinationPath.toString(), srcFolder)) { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - if (dir.equals(resourcesPath)) { + if (dir.equals(pythonResources) || dir.equals(pythonTestResources) || dir.equals(pythonTestResourcesLegacy)) { return FileVisitResult.SKIP_SUBTREE; } return super.preVisitDirectory(dir, attrs); @@ -82,7 +79,6 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th } } - Path sourcePath = Paths.get(inputDir.getAbsolutePath(), appName, language); if (!Files.exists(sourcePath)) { sourcePath.toFile().mkdir(); } @@ -94,6 +90,71 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th } } + private static void copySharedPythonResources(Path srcPath, Path sourcePath, Path destinationPath) throws IOException { + Path pythonConfigPath = sourcePath.resolve("config"); + copySharedPythonResourceDirectory(srcPath.resolve("main/resources"), destinationPath.resolve("config"), pythonConfigPath); + Path pythonTestConfigPath = sourcePath.resolve("tests-config"); + copySharedPythonResourceDirectory(srcPath.resolve("test/resources"), destinationPath.resolve("tests-config"), pythonTestConfigPath); + copySharedPythonResourceDirectory(srcPath.resolve("test-resources"), destinationPath.resolve("tests-config"), pythonTestConfigPath); + } + + private static void copySharedPythonResourceDirectory(Path resourcePath, Path destinationPath, Path pythonSpecificPath) throws IOException { + if (!Files.exists(resourcePath)) { + return; + } + try (Stream paths = Files.walk(resourcePath)) { + paths.filter(Files::isRegularFile).forEach(source -> { + try { + Path relative = resourcePath.relativize(source); + // Python samples use application configuration and native config imports. + if (GuideUtils.isBootstrapConfiguration(relative)) { + return; + } + if (isOverriddenByPythonConfig(relative, pythonSpecificPath)) { + return; + } + Path destination = destinationPath.resolve(relative); + Files.createDirectories(destination.getParent()); + Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } + + private static boolean isOverriddenByPythonConfig(Path relative, Path pythonSpecificPath) throws IOException { + if (Files.exists(pythonSpecificPath.resolve(relative))) { + return true; + } + if (relative.getNameCount() != 1 || !Files.isDirectory(pythonSpecificPath)) { + return false; + } + String filename = relative.getFileName().toString(); + int extensionIndex = filename.lastIndexOf('.'); + if (extensionIndex < 1) { + return false; + } + String basename = filename.substring(0, extensionIndex); + if (!isEnvironmentConfigBasename(basename)) { + return false; + } + try (Stream paths = Files.list(pythonSpecificPath)) { + return paths.filter(Files::isRegularFile) + .map(path -> path.getFileName().toString()) + .anyMatch(name -> name.startsWith(basename + ".")); + } + } + + private static boolean isEnvironmentConfigBasename(String basename) { + return basename.equals("application") + || basename.startsWith("application-") + || basename.equals("bootstrap") + || basename.startsWith("bootstrap-"); + } + private static File fileToDelete(File destination, String path) { return Paths.get(destination.getAbsolutePath(), path).toFile(); } @@ -126,10 +187,10 @@ public void transferFiles(@NotNull @NonNull File inputDirectory, @NotNull @NonNu if (guide.base() != null) { File baseDir = new File(inputDirectory.getParentFile(), guide.base()); - copyGuideSourceFiles(baseDir, destinationPath, appName, guidesOption.getLanguage().toString(), true); + copyGuideSourceFiles(baseDir, destinationPath, appName, guidesOption.getLanguage(), true); } - copyGuideSourceFiles(inputDirectory, destinationPath, appName, guidesOption.getLanguage().toString(), false); + copyGuideSourceFiles(inputDirectory, destinationPath, appName, guidesOption.getLanguage(), false); if (app.excludeSource() != null) { for (String mainSource : app.excludeSource()) { @@ -154,6 +215,12 @@ public void transferFiles(@NotNull @NonNull File inputDirectory, @NotNull @NonNu if (f.exists()) { f.delete(); } + if (guidesOption.getBuildTool() == PYRONAUT && guidesOption.getLanguage() == PYTHON) { + f = new File(destination, "tests/" + MacroUtils.pythonTestModuleName(testSource) + "." + guidesOption.getLanguage().getExtension()); + if (f.exists()) { + f.delete(); + } + } } } diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideParser.java b/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideParser.java index 85668b70b71..7175a2fc6e9 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideParser.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideParser.java @@ -19,6 +19,8 @@ import java.util.*; import static io.micronaut.guides.core.GuideUtils.mergeMetadataList; +import static io.micronaut.starter.options.BuildTool.PYRONAUT; +import static io.micronaut.starter.options.Language.PYTHON; @Singleton public class DefaultGuideParser implements GuideParser { @@ -106,9 +108,19 @@ public Optional parseGuideMetadata(@NonNull @NotNull File guidesDir, @Non app.testFramework(), app.excludeTest(), app.excludeSource(), - app.validateLicense() + app.validateLicense(), + app.pythonFeatures() != null ? app.pythonFeatures() : new ArrayList<>() )); } + List languages = raw.languages() != null + ? new ArrayList<>(raw.languages()) + : new ArrayList<>(List.of(Language.JAVA, Language.GROOVY, Language.KOTLIN)); + List buildTools = raw.buildTools() != null + ? new ArrayList<>(raw.buildTools()) + : new ArrayList<>(List.of(BuildTool.GRADLE, BuildTool.MAVEN)); + if (languages.contains(PYTHON) && !buildTools.contains(PYRONAUT)) { + buildTools.add(PYRONAUT); + } return Optional.of(new Guide( raw.title(), @@ -119,19 +131,20 @@ public Optional parseGuideMetadata(@NonNull @NotNull File guidesDir, @Non raw.minimumJavaVersion(), raw.maximumJavaVersion(), raw.cloud(), - raw.skipGradleTests(), - raw.skipMavenTests(), + Boolean.TRUE.equals(raw.skipGradleTests()), + Boolean.TRUE.equals(raw.skipMavenTests()), publish ? guidesDir.getName() + ".adoc" : null, - raw.languages() != null ? raw.languages() : List.of(Language.JAVA, Language.GROOVY, Language.KOTLIN), + languages, raw.tags() != null ? raw.tags() : Collections.emptyList(), - raw.buildTools() != null ? raw.buildTools() : List.of(BuildTool.GRADLE, BuildTool.MAVEN), + buildTools, raw.testFramework(), raw.zipIncludes() != null ? raw.zipIncludes() : new ArrayList<>(), guidesDir.getName(), publish, raw.base(), raw.env() != null ? raw.env() : new HashMap<>(), - apps + apps, + Boolean.TRUE.equals(raw.skipPyronautTests()) )); } } diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideProjectGenerator.java b/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideProjectGenerator.java index ce493ddaaf9..63b4cbf32cf 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideProjectGenerator.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideProjectGenerator.java @@ -24,16 +24,22 @@ import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Stream; import static io.micronaut.core.util.StringUtils.EMPTY_STRING; import static io.micronaut.http.HttpStatus.BAD_REQUEST; import static io.micronaut.starter.options.BuildTool.GRADLE; +import static io.micronaut.starter.options.BuildTool.PYRONAUT; import static io.micronaut.starter.options.JdkVersion.JDK_25; +import static io.micronaut.starter.options.Language.GROOVY; +import static io.micronaut.starter.options.Language.PYTHON; @Singleton public class DefaultGuideProjectGenerator implements GuideProjectGenerator { @@ -86,7 +92,8 @@ public void generate(@NonNull File outputDirectory, @NonNull JdkVersion javaVersion, @NonNull App app) throws IOException { List appFeatures = new ArrayList<>(GuideUtils.getAppFeatures(app, guidesOption.getLanguage())); - if (!guidesConfiguration.getJdkVersionsSupportedByGraalvm().contains(javaVersion)) { + if (guidesOption.getLanguage() == GROOVY || + !guidesConfiguration.getJdkVersionsSupportedByGraalvm().contains(javaVersion)) { appFeatures.remove("graalvm"); } @@ -114,12 +121,31 @@ public void generate(@NonNull File outputDirectory, generatorContext.getProject(), new FileSystemOutputHandler(destination, ConsoleOutput.NOOP), generatorContext); + if (guidesOption.getBuildTool() == PYRONAUT && guidesOption.getLanguage() == PYTHON) { + removePythonPackageMarkerFiles(destination); + } } catch (Exception e) { LOG.error("Error generating application: " + e.getMessage(), e); throw new IOException(e.getMessage(), e); } } + private static void removePythonPackageMarkerFiles(File destination) throws IOException { + try (Stream paths = Files.walk(destination.toPath())) { + paths.filter(Files::isRegularFile) + .filter(path -> "__init__.py".equals(path.getFileName().toString())) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } + private GeneratorContext createProjectGeneratorContext( ApplicationType type, @Pattern(regexp = "[\\w\\d-_\\.]+") String packageAndName, diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/DefaultTestScriptGenerator.java b/buildSrc/src/main/java/io/micronaut/guides/core/DefaultTestScriptGenerator.java index 4745db2126b..8814a7d823a 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/DefaultTestScriptGenerator.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/DefaultTestScriptGenerator.java @@ -10,6 +10,9 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -80,13 +83,7 @@ private static String scriptForFolder(String nestedFolder, } if (buildTool == PYRONAUT) { - bashScript.append( - """ - pyronaut install || EXIT_STATUS=$? - pyronaut validate-config || EXIT_STATUS=$? - pyronaut test || EXIT_STATUS=$? - """ - ); + bashScript.append("run_pyronaut_tests || EXIT_STATUS=$?\n"); } else if (nativeTest) { bashScript.append(String.format( "%s || EXIT_STATUS=$?\n", @@ -181,6 +178,11 @@ public String generateTestScript(@NonNull @NotNull List metadatas) { return generateScript(metadatas, false, false); } + @Override + public String generatePythonTestScript(@NonNull @NotNull List metadatas) { + return generateScript(metadatas, false, false, true); + } + public String generateScript(File guidesFolder, String metadataConfigName, boolean stopIfFailure, @@ -203,13 +205,20 @@ public String generateScript(File guidesFolder, public String generateScript(List metadatas, boolean stopIfFailure, boolean nativeTest) { + return generateScript(metadatas, stopIfFailure, nativeTest, false); + } + + public String generateScript(List metadatas, + boolean stopIfFailure, + boolean nativeTest, + boolean pythonTest) { StringBuilder bashScript = new StringBuilder(""" #!/usr/bin/env bash set -e - + FAILED_PROJECTS=() EXIT_STATUS=0 - + kill_kotlin_daemon () { echo "Killing KotlinCompile daemon to pick up fresh properties (due to kapt and java > 17)" for daemon in $(jps | grep KotlinCompile | cut -d' ' -f1); do @@ -217,12 +226,21 @@ public String generateScript(List metadatas, kill -9 $daemon done }"""); + if (pythonTest) { + bashScript.append("\n\n").append(pyronautFunctions()); + } metadatas.sort(Comparator.comparing(Guide::slug)); for (Guide metadata : metadatas) { + if (metadata.apps().isEmpty()) { + continue; + } List guidesOptionList = GuideGenerationUtils.guidesOptions(metadata, LOG); bashScript.append("\n"); for (GuidesOption guidesOption : guidesOptionList) { + if (pythonTest != isPyronautPython(guidesOption)) { + continue; + } String folder = MacroUtils.getSourceDir(metadata.slug(), guidesOption); BuildTool buildTool = guidesOption.getBuildTool(); if (metadata.apps().stream().anyMatch(app -> app.name().equals(guidesConfiguration.getDefaultAppName()))) { @@ -271,10 +289,25 @@ public String generateScript(List metadatas, else exit 0 fi - + """); } return bashScript.toString(); } + + private static String pyronautFunctions() { + try (InputStream stream = DefaultTestScriptGenerator.class.getResourceAsStream("/pyronaut-test-functions.sh")) { + if (stream == null) { + throw new IllegalStateException("Missing pyronaut-test-functions.sh resource"); + } + return new String(stream.readAllBytes(), StandardCharsets.UTF_8).stripTrailing() + "\n"; + } catch (IOException e) { + throw new IllegalStateException("Unable to read pyronaut-test-functions.sh resource", e); + } + } + + private static boolean isPyronautPython(GuidesOption guidesOption) { + return guidesOption.getBuildTool() == PYRONAUT && guidesOption.getLanguage() == Language.PYTHON; + } } diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/DependencyLines.java b/buildSrc/src/main/java/io/micronaut/guides/core/DependencyLines.java index a70fae27d5d..6b5faf3647b 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/DependencyLines.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/DependencyLines.java @@ -8,6 +8,7 @@ import static io.micronaut.starter.options.BuildTool.GRADLE; import static io.micronaut.starter.options.BuildTool.MAVEN; +import static io.micronaut.starter.options.BuildTool.PYRONAUT; import static io.micronaut.starter.options.Language.GROOVY; import static io.micronaut.starter.options.Language.KOTLIN; @@ -58,15 +59,29 @@ private static String toGradleScope(Map attributes, Language lan } } + private static String toPyronautScope(Map attributes) { + String s = attributes.get("scope"); + if (s == null) { + return "runtime"; + } + return switch (s) { + case "annotationProcessor", "compileOnly" -> "build"; + case "testCompile", "test", "testRuntimeOnly", "testImplementation", "testCompileOnly", "testAnnotationProcessor" -> "test"; + default -> "runtime"; + }; + } + public static List asciidoc(String line, BuildTool buildTool, Language language) { return asciidoc(Collections.singletonList(line), buildTool, language); } public static List asciidoc(List lines, BuildTool buildTool, Language language) { + if (buildTool == PYRONAUT) { + return pyronautAsciidoc(lines); + } if (buildTool != GRADLE && buildTool != MAVEN) { return List.of(); } - List dependencyLines = new ArrayList<>(); // Open Asciidoctor code block @@ -154,6 +169,49 @@ public static List asciidoc(List lines, BuildTool buildTool, Lan return dependencyLines; } + private static List pyronautAsciidoc(List lines) { + Map> dependencies = new LinkedHashMap<>(); + dependencies.put("runtime", new ArrayList<>()); + dependencies.put("build", new ArrayList<>()); + dependencies.put("test", new ArrayList<>()); + + for (String line : lines) { + String artifactId = line.substring("dependency:".length(), line.indexOf("[")); + Map attributes = new HashMap<>(); + String attributesStr = line.substring(line.indexOf("[") + "[".length(), line.indexOf("]")); + String[] attrs = attributesStr.split(","); + for (String att : attrs) { + String[] keyValues = att.split("="); + if (keyValues.length == 2) { + attributes.put(keyValues[0], keyValues[1]); + } + } + + String groupId = attributes.getOrDefault("groupId", "io.micronaut"); + String version = attributes.get("version"); + String coordinate = groupId + ':' + artifactId + (version != null ? ':' + version : ""); + String callout = extractCallout(attributes); + String tomlCallout = callout.isEmpty() ? "" : " #" + callout.substring(" //".length()); + dependencies.get(toPyronautScope(attributes)).add("\"" + coordinate + "\"," + tomlCallout); + } + + List dependencyLines = new ArrayList<>(); + dependencyLines.add("[source, toml]"); + dependencyLines.add(".pyproject.toml"); + dependencyLines.add("----"); + dependencyLines.add("[tool.pyronaut.dependencies]"); + for (Map.Entry> entry : dependencies.entrySet()) { + if (entry.getValue().isEmpty()) { + continue; + } + dependencyLines.add(entry.getKey() + " = ["); + entry.getValue().forEach(value -> dependencyLines.add(" " + value)); + dependencyLines.add("]"); + } + dependencyLines.add("----"); + return dependencyLines; + } + private static String extractCallout(Map attributes) { String callout = attributes.getOrDefault("callout", null); return callout != null ? " // <" + callout + ">" : ""; diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/Guide.java b/buildSrc/src/main/java/io/micronaut/guides/core/Guide.java index b9053ec7ac1..b40b7e94f03 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/Guide.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/Guide.java @@ -38,7 +38,8 @@ * @param publish Whether the guide should be published, it defaults to true. You can set it to false for draft or base guides * @param base Defaults to null; if set, indicates directory name of the base guide to copy before copying the current one * @param env The guide's environment variables - * @param apps Applications created for the guide + * @param apps Applications created for the guide; an empty list denotes a documentation-only guide with no generated application + * @param skipPyronautTests Set it to true to skip running the Pyronaut application tests for the guide */ @JsonSchema @Serdeable @@ -111,8 +112,38 @@ public record Guide( @Nullable Map env, - @NotEmpty + @NotNull @NonNull - List apps + List apps, + + @JsonProperty(defaultValue = StringUtils.FALSE) + @Nullable + Boolean skipPyronautTests ) { + public Guide( + @NonNull String title, + @NonNull String intro, + @NonNull List authors, + @NonNull List categories, + @NonNull LocalDate publicationDate, + @Nullable Integer minimumJavaVersion, + @Nullable Integer maximumJavaVersion, + @Nullable Cloud cloud, + @Nullable Boolean skipGradleTests, + @Nullable Boolean skipMavenTests, + @Nullable String asciidoctor, + @Nullable List languages, + @Nullable List tags, + @Nullable List buildTools, + @Nullable TestFramework testFramework, + @Nullable List zipIncludes, + @Nullable String slug, + @Nullable Boolean publish, + @Nullable String base, + @Nullable Map env, + @NonNull List apps) { + this(title, intro, authors, categories, publicationDate, minimumJavaVersion, maximumJavaVersion, cloud, + skipGradleTests, skipMavenTests, asciidoctor, languages, tags, buildTools, testFramework, zipIncludes, + slug, publish, base, env, apps, false); + } } diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/GuideGenerationUtils.java b/buildSrc/src/main/java/io/micronaut/guides/core/GuideGenerationUtils.java index d4bb1bd24ba..84c1ae94d70 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/GuideGenerationUtils.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/GuideGenerationUtils.java @@ -60,6 +60,17 @@ static String pathByFolder(@NonNull String appName, @NonNull GuidesOption option, @NonNull GuidesConfiguration configuration) { String module = StringUtils.isNotEmpty(appName) ? appName + "/" : ""; + if (MacroUtils.isPyronautPython(option)) { + String sourceFolder = folder.equals("test") ? "tests" : "src"; + String target = folder.equals("test") + ? MacroUtils.pythonTestModuleName(fileName) + : MacroUtils.pythonModuleName(fileName); + Path path = Path.of(module, + sourceFolder, + configuration.getPackageName().replace(".", "/"), + target + "." + option.getLanguage().getExtension()); + return path.toString(); + } Path path = Path.of(module, "src", folder, diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/GuideUtils.java b/buildSrc/src/main/java/io/micronaut/guides/core/GuideUtils.java index 661298b0a5d..656f75dbc4c 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/GuideUtils.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/GuideUtils.java @@ -5,9 +5,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; +import static io.micronaut.starter.options.BuildTool.PYRONAUT; +import static io.micronaut.starter.options.Language.PYTHON; + public final class GuideUtils { private static final Logger LOG = LoggerFactory.getLogger(GuideUtils.class); @@ -20,6 +24,16 @@ public final class GuideUtils { private GuideUtils() { } + /** + * Identifies root configuration files that Python samples must not inherit from JVM resources. + * @param relative The resource's path relative to its configuration directory + * @return Whether this is a bootstrap configuration file + */ + public static boolean isBootstrapConfiguration(Path relative) { + return relative.getNameCount() == 1 + && relative.getFileName().toString().matches("bootstrap(?:-[^.]+)?\\.(properties|yml|yaml|json|toml)"); + } + public static List getTags(Guide guide) { Set tagsList = new HashSet<>(); if (guide.tags() != null) { @@ -32,6 +46,7 @@ public static List getTags(Guide guide) { addAllSafe(allFeatures, app.javaFeatures()); addAllSafe(allFeatures, app.kotlinFeatures()); addAllSafe(allFeatures, app.groovyFeatures()); + addAllSafe(allFeatures, app.pythonFeatures()); for (String featureName : allFeatures) { String tagToAdd = featureName; for (String prefix : FEATURES_PREFIXES) { @@ -57,6 +72,9 @@ public static List getAppFeatures(App app, Language language) { if (language == Language.GROOVY) { return mergeLists(app.features(), getAppInvisibleFeatures(app), app.jvmFeatures(), app.groovyFeatures()); } + if (language == Language.PYTHON) { + return mergeLists(app.features(), getAppInvisibleFeatures(app), app.pythonFeatures()); + } return mergeLists(app.features(), getAppInvisibleFeatures(app)); } @@ -80,6 +98,9 @@ public static List getAppVisibleFeatures(App app, Language language) { if (language == Language.GROOVY) { return mergeLists(app.features(), app.jvmFeatures(), app.groovyFeatures()); } + if (language == Language.PYTHON) { + return mergeLists(app.features(), app.pythonFeatures()); + } return app.features(); } @@ -100,6 +121,9 @@ public static boolean shouldSkip(Guide guide, BuildTool buildTool, Language lang if (buildTool == BuildTool.MAVEN) { return guide.skipMavenTests(); } + if (buildTool == BuildTool.PYRONAUT) { + return guide.skipPyronautTests(); + } return false; } @@ -109,6 +133,21 @@ public static Set getFrameworks(Guide guide) { } public static Guide merge(Guide base, Guide guide) { + List languages = new ArrayList<>(); + if (guide.languages() != null) { + languages.addAll(guide.languages()); + } else if (base.languages() != null) { + languages.addAll(base.languages()); + } + List buildTools = new ArrayList<>(); + if (guide.buildTools() != null) { + buildTools.addAll(guide.buildTools()); + } else if (base.buildTools() != null) { + buildTools.addAll(base.buildTools()); + } + if (languages.contains(PYTHON) && !buildTools.contains(PYRONAUT)) { + buildTools.add(PYRONAUT); + } return new Guide( guide.title() == null ? base.title() : guide.title(), guide.intro() == null ? base.intro() : guide.intro(), @@ -121,16 +160,17 @@ public static Guide merge(Guide base, Guide guide) { base.skipGradleTests() || guide.skipGradleTests(), base.skipMavenTests() || guide.skipMavenTests(), guide.asciidoctor(), - guide.languages() == null ? base.languages() : guide.languages(), + languages, mergeLists(GuideUtils.getTags(base), GuideUtils.getTags(guide)), - guide.buildTools() == null ? base.buildTools() : guide.buildTools(), + buildTools, guide.testFramework() == null ? base.testFramework() : guide.testFramework(), guide.zipIncludes(), guide.slug(), guide.publish(), guide.base(), guide.env() == null ? base.env() : guide.env(), - mergeApps(base.apps(), guide.apps()) + mergeApps(base.apps(), guide.apps()), + Boolean.TRUE.equals(base.skipPyronautTests()) || Boolean.TRUE.equals(guide.skipPyronautTests()) ); } @@ -175,7 +215,8 @@ private static List mergeApps(List base, List guide) { guideApp.testFramework(), guideApp.excludeTest(), guideApp.excludeSource(), - baseApp.validateLicense() + baseApp.validateLicense(), + mergeLists(guideApp.pythonFeatures(), baseApp.pythonFeatures()) ); merged.add(mergedApp); } diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/MacroUtils.java b/buildSrc/src/main/java/io/micronaut/guides/core/MacroUtils.java index 3477e818522..a73e3ba5c7a 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/MacroUtils.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/MacroUtils.java @@ -1,10 +1,13 @@ package io.micronaut.guides.core; import io.micronaut.core.annotation.NonNull; +import io.micronaut.starter.options.BuildTool; +import io.micronaut.starter.options.Language; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -18,6 +21,25 @@ static String getSourceDir(@NonNull String slug, @NonNull GuidesOption option) { return slug + "-" + option.getBuildTool() + "-" + option.getLanguage(); } + static boolean isPyronautPython(@NonNull GuidesOption option) { + return option.getBuildTool() == BuildTool.PYRONAUT && option.getLanguage() == Language.PYTHON; + } + + static String pythonModuleName(@NonNull String target) { + if (target.equals(target.toLowerCase(Locale.ROOT))) { + return target; + } + return target.replaceAll("([a-z0-9])([A-Z])", "$1_$2") + .replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2") + .toLowerCase(Locale.ROOT); + } + + static String pythonTestModuleName(@NonNull String target) { + String normalized = target.endsWith("Test") ? target.substring(0, target.length() - "Test".length()) : target; + normalized = pythonModuleName(normalized); + return normalized.startsWith("test_") ? normalized : "test_" + normalized; + } + static List findMacroLines(@NonNull String str, @NonNull String macro) { return str.lines() .filter(line -> line.startsWith(macro + ":")) diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/SourceBlockMacroSubstitution.java b/buildSrc/src/main/java/io/micronaut/guides/core/SourceBlockMacroSubstitution.java index 720b968ace6..94b5a179c83 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/SourceBlockMacroSubstitution.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/SourceBlockMacroSubstitution.java @@ -60,6 +60,7 @@ public String substitute(String str, Guide guide, GuidesOption option) { language = arr[arr.length - 1]; language = resolveAsciidoctorLanguage(language); } else { + condensedTarget = sourceTarget(condensedTarget, getClasspath(), option); condensedTarget = condensedTarget + "." + extension; } @@ -68,7 +69,7 @@ public String substitute(String str, Guide guide, GuidesOption option) { IncludeDirective.Builder includeDirectiveBuilder = IncludeDirective.builder().attributes(asciidocMacro.attributes()) .target(target); - if (getFileType() == FileType.CODE) { + if (getFileType() == FileType.CODE && !isPyronautPython(option)) { Range range = new Range(getLicenseLoader().getNumberOfLines(), -1); if (range.isValid() && asciidocMacro.attributes().stream().noneMatch(attribute -> attribute.key().equals(ATTRIBUTE_LINES))) { includeDirectiveBuilder.lines(range); @@ -94,17 +95,6 @@ protected String getExtension(GuidesOption option) { return option.getLanguage().getExtension(); } - protected String sourceTitle( - String appName, - String condensedTarget, - Classpath classpath, - String language, - String packageName) { - return (appName.equals(MacroSubstitution.APP_NAME_DEFAULT) ? "" : (appName + "/")) + sourceConventionFolder(classpath, language) + "/" - + (getFileType() == FileType.CODE ? (packageName.replace(".", "/") + "/") : "") - + condensedTarget; - } - protected String sourceInclude( String slug, String appName, @@ -114,10 +104,18 @@ protected String sourceInclude( String language, String packageName) { return "{sourceDir}/" + slug + "/" + getSourceDir(slug, option) + "/" + - sourceTitle(appName, condensedTarget, classpath, language, packageName); + sourceTitle(appName, condensedTarget, classpath, language, packageName, option); } - private String sourceConventionFolder(Classpath classpath, String language) { + private String sourceConventionFolder(Classpath classpath, String language, GuidesOption option) { + if (option != null && isPyronautPython(option)) { + if (getFileType() == FileType.CODE) { + return classpath == Classpath.TEST ? "tests" : "src"; + } + if (getFileType() == FileType.RESOURCE) { + return classpath == Classpath.TEST ? "tests-config" : "config"; + } + } if (getFileType() == FileType.CODE) { return "src/" + classpath + "/" + language; } else if (getFileType() == FileType.RESOURCE) { @@ -126,7 +124,26 @@ private String sourceConventionFolder(Classpath classpath, String language) { throw new UnsupportedOperationException("Unimplemented sourceConventionFolder for " + getFileType()); } + private String sourceTarget(String target, Classpath classpath, GuidesOption option) { + if (isPyronautPython(option) && getFileType() == FileType.CODE) { + return classpath == Classpath.TEST ? pythonTestModuleName(target) : pythonModuleName(target); + } + return target; + } + + protected String sourceTitle( + String appName, + String condensedTarget, + Classpath classpath, + String language, + String packageName, + GuidesOption option) { + return (appName.equals(MacroSubstitution.APP_NAME_DEFAULT) ? "" : (appName + "/")) + sourceConventionFolder(classpath, language, option) + "/" + + (getFileType() == FileType.CODE ? (packageName.replace(".", "/") + "/") : "") + + condensedTarget; + } + protected String condensedTarget(@NotNull AsciidocMacro asciidocMacro, GuidesOption option) { return asciidocMacro.target(); } -} \ No newline at end of file +} diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/TestScriptGenerator.java b/buildSrc/src/main/java/io/micronaut/guides/core/TestScriptGenerator.java index 0ff42ac94c9..6d888fd21dc 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/TestScriptGenerator.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/TestScriptGenerator.java @@ -22,4 +22,8 @@ public interface TestScriptGenerator { @NotNull String generateTestScript(@NonNull @NotNull List metadatas); + @NonNull + @NotNull + String generatePythonTestScript(@NonNull @NotNull List metadatas); + } diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/ZipIncludeMacroSubstitution.java b/buildSrc/src/main/java/io/micronaut/guides/core/ZipIncludeMacroSubstitution.java index f73bd5505d7..1938e768732 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/ZipIncludeMacroSubstitution.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/ZipIncludeMacroSubstitution.java @@ -32,7 +32,8 @@ protected String sourceTitle( String condensedTarget, Classpath classpath, String language, - String packageName) { + String packageName, + GuidesOption option) { return condensedTarget; } } diff --git a/buildSrc/src/main/java/io/micronaut/guides/feature/H2Runtime.java b/buildSrc/src/main/java/io/micronaut/guides/feature/H2Runtime.java new file mode 100644 index 00000000000..be704798909 --- /dev/null +++ b/buildSrc/src/main/java/io/micronaut/guides/feature/H2Runtime.java @@ -0,0 +1,11 @@ +package io.micronaut.guides.feature; + +import io.micronaut.starter.build.dependencies.Scope; +import jakarta.inject.Singleton; + +@Singleton +public class H2Runtime extends AbstractFeature { + protected H2Runtime() { + super("h2-runtime", "h2", Scope.RUNTIME); + } +} diff --git a/buildSrc/src/main/java/io/micronaut/guides/feature/SpringSecurityCrypto.java b/buildSrc/src/main/java/io/micronaut/guides/feature/SpringSecurityCrypto.java new file mode 100644 index 00000000000..7c5900ef406 --- /dev/null +++ b/buildSrc/src/main/java/io/micronaut/guides/feature/SpringSecurityCrypto.java @@ -0,0 +1,27 @@ +package io.micronaut.guides.feature; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.starter.application.generator.GeneratorContext; +import io.micronaut.starter.build.dependencies.Dependency; +import io.micronaut.starter.build.dependencies.Scope; +import jakarta.inject.Singleton; + +@Requires(missingClasses = "io.micronaut.starter.feature.security.SpringSecurityCrypto") +@Singleton +public class SpringSecurityCrypto extends AbstractFeature { + + public SpringSecurityCrypto() { + super("spring-security-crypto"); + } + + @Override + public void apply(GeneratorContext generatorContext) { + super.apply(generatorContext); + + generatorContext.addDependency(Dependency.builder() + .groupId("org.slf4j") + .artifactId("jcl-over-slf4j") + .scope(Scope.RUNTIME) + .build()); + } +} diff --git a/buildSrc/src/main/resources/guide-metadata.schema.json b/buildSrc/src/main/resources/guide-metadata.schema.json index 19da9c78385..9fc035876a5 100644 --- a/buildSrc/src/main/resources/guide-metadata.schema.json +++ b/buildSrc/src/main/resources/guide-metadata.schema.json @@ -46,6 +46,14 @@ "description": "Set it to true to skip running the tests for the Maven applications for the guide", "type": "boolean" }, + "skipPyronautTests": { + "description": "Set it to true to skip running the tests for the Pyronaut applications for the guide", + "type": "boolean" + }, + "python": { + "description": "Set it to true to generate a Python/Pyronaut variant for the guide", + "type": "boolean" + }, "asciidoctor": { "description": "The guide asciidoc file. If not specified, the guide slug followed by the .adoc suffix is used", "type": "string" @@ -109,7 +117,7 @@ "type": "object" }, "apps": { - "description": "Applications created for the guide.", + "description": "Applications created for the guide; an empty list denotes a documentation-only guide with no generated application.", "type": "array", "items": { "type": "object", @@ -166,6 +174,13 @@ "type": "string" } }, + "pythonFeatures": { + "description": "The app's Python features", + "type": "array", + "items": { + "type": "string" + } + }, "jvmFeatures": { "description": "The app's features shared by the JVM languages (Java, Kotlin and Groovy)", "type": "array", diff --git a/buildSrc/src/main/resources/pom.xml b/buildSrc/src/main/resources/pom.xml index c69fd8a95d3..1595ddda82f 100644 --- a/buildSrc/src/main/resources/pom.xml +++ b/buildSrc/src/main/resources/pom.xml @@ -12,6 +12,10 @@ + + com.h2database + h2 + org.skyscreamer jsonassert diff --git a/buildSrc/src/main/resources/pyronaut-test-functions.sh b/buildSrc/src/main/resources/pyronaut-test-functions.sh new file mode 100644 index 00000000000..1d937912192 --- /dev/null +++ b/buildSrc/src/main/resources/pyronaut-test-functions.sh @@ -0,0 +1,223 @@ +pyronaut_repo_python () { + if [ -n "${PYTHON:-}" ]; then + echo "$PYTHON" + elif command -v python3 > /dev/null 2>&1; then + echo "python3" + else + echo "python" + fi +} + +set_pyronaut_local_repositories () { + if [ -z "${PYRONAUT_LOCAL_REPOSITORY:-}" ]; then + return 0 + fi + + "$(pyronaut_repo_python)" -c 'exec(__import__("sys").stdin.read())' "$PYRONAUT_LOCAL_REPOSITORY" <<'PY' +from pathlib import Path +import json +import re +import sys + +repository = sys.argv[1] +pyproject = Path("pyproject.toml") +text = pyproject.read_text(encoding="utf-8") +section = "[tool.pyronaut]" +section_index = text.find(section) +if section_index < 0: + raise SystemExit("Missing [tool.pyronaut] in pyproject.toml") + +next_section_index = text.find("\n[", section_index + len(section)) +if next_section_index < 0: + next_section_index = len(text) +section_text = text[section_index:next_section_index] +repositories = f'repositories = [{json.dumps(repository)}, "mavenCentral", "https://central.sonatype.com/repository/maven-snapshots/"]' +section_text, count = re.subn(r'(?ms)^repositories\s*=\s*\[[^\]]*\]', repositories, section_text, count=1) +if count == 0: + section_text = section_text.replace(section + "\n", section + "\n" + repositories + "\n", 1) +text = text[:section_index] + section_text + text[next_section_index:] +pyproject.write_text(text, encoding="utf-8") +PY +} + +set_pyronaut_local_versions () { + if [ -z "${PYRONAUT_LOCAL_CORE_VERSION:-}" ] && [ -z "${PYRONAUT_LOCAL_PLATFORM_VERSION:-}" ]; then + return 0 + fi + + "$(pyronaut_repo_python)" -c 'exec(__import__("sys").stdin.read())' "${PYRONAUT_LOCAL_CORE_VERSION:-}" "${PYRONAUT_LOCAL_PLATFORM_VERSION:-}" <<'PY' +from pathlib import Path +import re +import sys + +core_version = sys.argv[1] +platform_version = sys.argv[2] +pyproject = Path("pyproject.toml") +text = pyproject.read_text(encoding="utf-8") + +def set_section_version(source, section, version): + if not version: + return source + section_index = source.find(section) + if section_index < 0: + raise SystemExit(f"Missing {section} in pyproject.toml") + + next_section_index = source.find("\n[", section_index + len(section)) + if next_section_index < 0: + next_section_index = len(source) + section_text = source[section_index:next_section_index] + replacement = f'version = "{version}"' + section_text, count = re.subn(r'(?m)^version\s*=\s*"[^"]*"', replacement, section_text, count=1) + if count == 0: + section_text = section_text.replace(section + "\n", section + "\n" + replacement + "\n", 1) + return source[:section_index] + section_text + source[next_section_index:] + +text = set_section_version(text, "[tool.pyronaut.core]", core_version) +text = set_section_version(text, "[tool.pyronaut.platform]", platform_version) +pyproject.write_text(text, encoding="utf-8") +PY +} + +set_pyronaut_test_resources () { + "$(pyronaut_repo_python)" -c 'exec(__import__("sys").stdin.read())' <<'PY' +from pathlib import Path +import re + +pyproject = Path("pyproject.toml") +text = pyproject.read_text(encoding="utf-8") +section = "[tool.pyronaut.test-resources]" +section_index = text.find(section) +if section_index < 0: + raise SystemExit(f"Missing {section} in pyproject.toml") + +next_section_index = text.find("\n[", section_index + len(section)) +if next_section_index < 0: + next_section_index = len(text) +section_text = text[section_index:next_section_index] +section_text, count = re.subn(r'(?m)^enabled\s*=\s*(?:true|false)', "enabled = true", section_text, count=1) +if count == 0: + section_text = section_text.replace(section + "\n", section + "\nenabled = true\n", 1) +text = text[:section_index] + section_text + text[next_section_index:] +pyproject.write_text(text, encoding="utf-8") +PY +} + +set_pyronaut_native_toolchain () { + if [ "${PYRONAUT_USE_NATIVE_TOOLCHAIN:-}" != "true" ]; then + return 0 + fi + + "$(pyronaut_repo_python)" -c 'exec(__import__("sys").stdin.read())' <<'PY' +from pathlib import Path + +pyproject = Path("pyproject.toml") +text = pyproject.read_text(encoding="utf-8") +section = "[tool.pyronaut.toolchain]" +section_index = text.find(section) +if section_index < 0: + separator = "" if not text or text.endswith("\n") else "\n" + text += f'{separator}\n{section}\ntype = "native"\n' +else: + next_section_index = text.find("\n[", section_index + len(section)) + if next_section_index < 0: + next_section_index = len(text) + section_text = text[section_index:next_section_index] + lines = section_text.splitlines(keepends=True) + for index, line in enumerate(lines): + if line.lstrip().startswith('type') and '=' in line: + line_ending = '\n' if line.endswith('\n') else '' + lines[index] = 'type = "native"' + line_ending + break + else: + lines.insert(1, 'type = "native"\n') + section_text = "".join(lines) + text = text[:section_index] + section_text + text[next_section_index:] +pyproject.write_text(text, encoding="utf-8") +PY +} + +set_pyronaut_project_venv () { + if [ -e .venv ] || [ -L .venv ] || [ -z "${VIRTUAL_ENV:-}" ] || [ ! -x "$VIRTUAL_ENV/bin/python" ]; then + return 0 + fi + ln -s "$VIRTUAL_ENV" .venv +} + +pyronaut_dependency_cache () { + echo "${PYRONAUT_DEPENDENCY_CACHE:-.pyronaut-m2}" +} + +run_pyronaut_install () { + local install_args=() + if [ -n "${PYRONAUT_LOCAL_REPOSITORY:-}" ]; then + install_args+=(--local-repository "$(pyronaut_dependency_cache)") + if [ "${PYRONAUT_REFRESH_DEPENDENCIES:-true}" = "true" ]; then + install_args+=(--refresh) + fi + fi + + if [ -n "${PYRONAUT_INSTALL_EXECUTABLE:-}" ]; then + "$PYRONAUT_INSTALL_EXECUTABLE" "${install_args[@]}" + else + pyronaut install "${install_args[@]}" + fi +} + +run_pyronaut_validate_config () { + if [ -n "${PYRONAUT_VALIDATE_CONFIG_EXECUTABLE:-}" ]; then + "$PYRONAUT_VALIDATE_CONFIG_EXECUTABLE" + else + pyronaut validate-config + fi +} + +run_pyronaut_process () { + if [ -n "${PYRONAUT_PROCESS_EXECUTABLE:-}" ]; then + "$PYRONAUT_PROCESS_EXECUTABLE" + else + pyronaut process + fi +} + +run_pyronaut_cli () { + if [ -n "${PYRONAUT_CLI_PYTHONPATH:-}" ] && [ -d "$PYRONAUT_CLI_PYTHONPATH" ]; then + PYTHONPATH="${PYRONAUT_CLI_PYTHONPATH}${PYTHONPATH:+:$PYTHONPATH}" "$(pyronaut_repo_python)" -m pyronaut_cli_v2 "$@" + else + pyronaut "$@" + fi +} + +run_pyronaut_test () { + local test_args=() + if [ -n "${PYRONAUT_LOCAL_REPOSITORY:-}" ]; then + test_args+=(--local-repository "$(pyronaut_dependency_cache)") + fi + run_pyronaut_cli test "${test_args[@]}" +} + +run_pyronaut_tests () { + if [ -z "${PYRONAUT_LOCAL_REPOSITORY:-}" ]; then + set_pyronaut_local_versions + set_pyronaut_test_resources + set_pyronaut_native_toolchain + set_pyronaut_project_venv + run_pyronaut_install && run_pyronaut_validate_config && run_pyronaut_process && run_pyronaut_test + return $? + fi + + local tmp_dir + tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/pyronaut-guide.XXXXXX")" + tar --exclude='./.micronaut' --exclude='./__pyronaut__' --exclude='./.pytest_cache' --exclude='./build' --exclude='./dist' --exclude='./*.egg-info' -cf - . | (cd "$tmp_dir" && tar -xf -) + ( + cd "$tmp_dir" + set_pyronaut_local_repositories + set_pyronaut_local_versions + set_pyronaut_test_resources + set_pyronaut_native_toolchain + set_pyronaut_project_venv + run_pyronaut_install && run_pyronaut_validate_config && run_pyronaut_process && run_pyronaut_test + ) + local result=$? + rm -rf "$tmp_dir" + return $result +} diff --git a/buildSrc/src/test/java/io/micronaut/guides/DocsOnlyGuideTest.java b/buildSrc/src/test/java/io/micronaut/guides/DocsOnlyGuideTest.java new file mode 100644 index 00000000000..163145b1a06 --- /dev/null +++ b/buildSrc/src/test/java/io/micronaut/guides/DocsOnlyGuideTest.java @@ -0,0 +1,133 @@ +package io.micronaut.guides; + +import io.micronaut.guides.core.App; +import io.micronaut.guides.core.Guide; +import io.micronaut.guides.core.GuideParser; +import io.micronaut.guides.tasks.SampleProjectGenerationTask; +import io.micronaut.starter.api.TestFramework; +import io.micronaut.starter.application.ApplicationType; +import io.micronaut.starter.options.BuildTool; +import io.micronaut.starter.options.Language; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.validation.validator.Validator; +import jakarta.inject.Inject; +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@MicronautTest(startApplication = false) +class DocsOnlyGuideTest { + + @Inject GuideParser guideParser; + @Inject Validator validator; + @Inject io.micronaut.guides.core.TestScriptGenerator coreScripts; + @TempDir Path tempDir; + + @Test + void explicitEmptyAppsAreValidMetadata() throws Exception { + writeMetadata("docs-only", "[]"); + Guide guide = guideParser.parseGuideMetadata(tempDir.resolve("guides/docs-only").toFile(), "metadata.json").orElseThrow(); + assertTrue(guide.apps().isEmpty()); + assertTrue(validator.validate(guide).isEmpty(), () -> validator.validate(guide).toString()); + assertFalse(guide.skipPyronautTests()); + } + + @Test + void emptyAppsProduceNoProjectOutputDirectories() throws Exception { + Guide guide = guide(List.of()); + assertAll( + () -> assertEquals(List.of(), SampleProjectGenerationTask.outputDirectoryNames(guide, null)), + () -> assertEquals(List.of(), SampleProjectGenerationTask.outputDirectoryNames(guide, Language.PYTHON)) + ); + try (GuideProjectGenerator generator = new GuideProjectGenerator()) { + generator.generateOne(guide, tempDir.toFile(), tempDir.resolve("code").toFile()); + } + try (var paths = Files.list(tempDir.resolve("code"))) { + assertEquals(0, paths.count()); + } + } + + @Test + void aggregateScriptsDoNotEnterDocsOnlyProjectDirectories() { + Guide guide = guide(List.of()); + assertAll( + () -> assertFalse(coreScripts.generateTestScript(new ArrayList<>(List.of(guide))).contains("cd docs-only-")), + () -> assertFalse(coreScripts.generateNativeTestScript(new ArrayList<>(List.of(guide))).contains("cd docs-only-")), + () -> assertFalse(coreScripts.generatePythonTestScript(new ArrayList<>(List.of(guide))).contains("cd docs-only-")), + () -> assertFalse(TestScriptGenerator.generateScript(new ArrayList<>(List.of(guide)), false, false, false, null).contains("cd docs-only-")), + () -> assertFalse(TestScriptGenerator.generateScript(new ArrayList<>(List.of(guide)), false, true, false, null).contains("cd docs-only-")), + () -> assertFalse(TestScriptGenerator.generateScript(new ArrayList<>(List.of(guide)), false, false, true, null).contains("cd docs-only-")) + ); + } + + @Test + void ordinaryAppsStillHaveOutputDirectoriesAndTestCommands() { + Guide guide = guide(List.of(new App("default", "example.micronaut", ApplicationType.DEFAULT, "Micronaut", + List.of(), List.of(), List.of(), List.of(), List.of(), null, null, null, false))); + assertEquals(List.of("docs-only-pyronaut-python"), SampleProjectGenerationTask.outputDirectoryNames(guide, Language.PYTHON)); + assertTrue(coreScripts.generatePythonTestScript(new ArrayList<>(List.of(guide))).contains("cd docs-only-pyronaut-python")); + assertTrue(TestScriptGenerator.generateScript(new ArrayList<>(List.of(guide)), false, false, true, null).contains("cd docs-only-pyronaut-python")); + } + + @Test + void docsOnlyTaskRegistrationRetainsDocsIndexAndBuildWithoutApplicationTasks() throws Exception { + writeMetadata("docs-only", "[]"); + writeMetadata("ordinary", "[{\"name\":\"default\"}]"); + Files.writeString(tempDir.resolve("guides/tests.properties"), "numberOfTestGroups=1\n"); + Project project = ProjectBuilder.builder().withProjectDir(tempDir.toFile()).build(); + project.getExtensions().getExtraProperties().set("metadataConfigName", "metadata.json"); + for (String name : List.of("asciidoctor", "themeGuides", "createDist", "generateTestScript", "generateGuidesIndex", "generateGuidesJsonMetadata")) { + project.getTasks().register(name); + } + new GuidesPlugin().apply(project); + + assertAll( + () -> assertNotNull(project.getTasks().findByName("docsOnlyGenerateDocs")), + () -> assertNotNull(project.getTasks().findByName("docsOnlyGenerateDocsJava")), + () -> assertNotNull(project.getTasks().findByName("docsOnlyGenerateDocsPython")), + () -> assertNotNull(project.getTasks().findByName("docsOnlyIndex")), + () -> assertNotNull(project.getTasks().findByName("docsOnlyBuild")), + () -> assertNotNull(project.getTasks().findByName("docsOnlyBuildJava")), + () -> assertNotNull(project.getTasks().findByName("docsOnlyBuildPython")), + () -> assertFalse(project.getTasks().getNames().stream().anyMatch(name -> name.startsWith("docsOnly") + && (name.contains("Zip") || name.contains("TestScript") || name.contains("Workflow")))), + () -> assertNotNull(project.getTasks().findByName("ordinaryPythonZipCode")), + () -> assertNotNull(project.getTasks().findByName("ordinaryRunPythonTestScript")), + () -> assertNotNull(project.getTasks().findByName("ordinaryGenerateGithubActionWorkflow")) + ); + } + + private void writeMetadata(String slug, String apps) throws Exception { + Path directory = tempDir.resolve("guides/" + slug); + Files.createDirectories(directory); + Files.writeString(directory.resolve("metadata.json"), """ + { + "title": "Documentation guide", + "intro": "Connect the existing application.", + "authors": ["Micronaut"], + "categories": ["Data JDBC"], + "publicationDate": "2026-10-06", + "languages": ["JAVA", "PYTHON"], + "apps": %s + } + """.formatted(apps)); + } + + private static Guide guide(List apps) { + return new Guide("Documentation guide", "Connect the existing application.", List.of("Micronaut"), + List.of("Data JDBC"), LocalDate.of(2026, 10, 6), null, null, null, false, false, + "docs-only.adoc", List.of(Language.JAVA, Language.PYTHON), List.of(), + List.of(BuildTool.GRADLE, BuildTool.MAVEN, BuildTool.PYRONAUT), TestFramework.JUNIT, + List.of(), "docs-only", true, null, Map.of(), apps, false); + } +} diff --git a/buildSrc/src/test/java/io/micronaut/guides/GuideAsciidocGeneratorTest.java b/buildSrc/src/test/java/io/micronaut/guides/GuideAsciidocGeneratorTest.java index afe086ab1c7..c35213d72e4 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/GuideAsciidocGeneratorTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/GuideAsciidocGeneratorTest.java @@ -2,6 +2,7 @@ import io.micronaut.guides.core.App; import io.micronaut.guides.core.Guide; +import io.micronaut.starter.api.TestFramework; import io.micronaut.starter.application.ApplicationType; import io.micronaut.starter.options.BuildTool; import io.micronaut.starter.options.Language; @@ -22,6 +23,41 @@ class GuideAsciidocGeneratorTest { @TempDir Path tempDir; + @Test + void onlyForLanguagesEndsAtClosingMarker() throws Exception { + Path input = Files.createDirectory(tempDir.resolve("guide")); + Path output = Files.createDirectory(tempDir.resolve("output")); + Path project = Files.createDirectory(tempDir.resolve("project")); + Files.createDirectories(project.resolve("buildSrc/src/main/resources")); + Files.writeString(project.resolve("buildSrc/src/main/resources/version.txt"), "5.2.0-SNAPSHOT"); + Files.createDirectories(project.resolve("src/docs/common/snippets")); + Files.writeString(project.resolve("src/docs/common/snippets/common-license.adoc"), "license"); + Files.writeString(input.resolve("guide.adoc"), """ + before + :only-for-languages:python + python-only + :only-for-languages: + after + """); + + Guide guide = new Guide( + "Guide", "Guide.", List.of("Micronaut"), List.of("Core Basics"), LocalDate.of(2026, 9, 22), + null, null, null, false, false, "guide.adoc", List.of(Language.JAVA, Language.PYTHON), List.of(), + List.of(BuildTool.GRADLE, BuildTool.PYRONAUT), TestFramework.JUNIT, List.of(), "guide", true, null, + Map.of(), List.of(new App("default", null, null, null, List.of(), null, null, null, null, null, null, null, true)), false); + + GuideAsciidocGenerator.generate(guide, input.toFile(), output.toFile(), project.toFile()); + + String java = Files.readString(output.resolve("guide-gradle-java.adoc")); + String python = Files.readString(output.resolve("guide-pyronaut-python.adoc")); + assertTrue(java.contains("before")); + assertFalse(java.contains("python-only")); + assertTrue(java.contains("after")); + assertTrue(python.contains("before")); + assertTrue(python.contains("python-only")); + assertTrue(python.contains("after")); + } + @Test void onlyForLanguagesFiltersContentAndUsesPythonProjectPaths() throws Exception { Path inputDir = Files.createDirectory(tempDir.resolve("input")); @@ -34,14 +70,14 @@ void onlyForLanguagesFiltersContentAndUsesPythonProjectPaths() throws Exception Authors: @authors@ Micronaut Version: @micronaut@ mn @cli-command@ example.micronaut.micronautguide - + :only-for-languages:python Python only source:HelloController[] test:HelloControllerTest[] pyronaut @cli-command@ example.micronaut.micronautguide --features=@features@ :only-for-languages: - + After """); diff --git a/buildSrc/src/test/java/io/micronaut/guides/GuideProjectGeneratorTest.java b/buildSrc/src/test/java/io/micronaut/guides/GuideProjectGeneratorTest.java new file mode 100644 index 00000000000..c68161566a2 --- /dev/null +++ b/buildSrc/src/test/java/io/micronaut/guides/GuideProjectGeneratorTest.java @@ -0,0 +1,76 @@ +package io.micronaut.guides; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GuideProjectGeneratorTest { + + @TempDir + Path directory; + + @Test + void pythonResourceTransferDoesNotInheritJvmBootstrapConfiguration() throws Exception { + File input = new File("src/test/resources/file-transfer/python-application-config"); + Path python = Files.createDirectory(directory.resolve("python")); + Path java = Files.createDirectory(directory.resolve("java")); + Method transfer = GuideProjectGenerator.class.getDeclaredMethod("copyGuideSourceFiles", + File.class, Path.class, String.class, String.class, boolean.class); + transfer.setAccessible(true); + + transfer.invoke(null, input, python, "", "python", false); + transfer.invoke(null, input, java, "", "java", false); + + assertTrue(Files.exists(python.resolve("config/application.toml"))); + assertTrue(Files.exists(python.resolve("tests-config/application-test.toml"))); + assertFalse(Files.exists(python.resolve("config/bootstrap.properties"))); + assertFalse(Files.exists(python.resolve("tests-config/bootstrap-test.properties"))); + assertTrue(Files.exists(java.resolve("src/main/resources/bootstrap.properties"))); + assertTrue(Files.exists(java.resolve("src/test/resources/bootstrap-test.properties"))); + } + + @Test + void packageCleanupDoesNotTraverseVirtualEnvironmentSymlinks() throws Exception { + Path project = Files.createDirectory(directory.resolve("project")); + Path external = Files.createDirectory(directory.resolve("site-packages")); + Files.createSymbolicLink(project.resolve(".venv"), external); + Path externalMarker = Files.writeString(external.resolve("__init__.py"), "package initializer\n"); + Path sourceMarker = Files.writeString(project.resolve("__init__.py"), ""); + Path linkedPackage = Files.createDirectory(project.resolve("linked-package")); + Path linkedMarker = Files.createSymbolicLink(linkedPackage.resolve("__init__.py"), externalMarker); + + Method cleanup = GuideProjectGenerator.class.getDeclaredMethod("removePythonPackageMarkerFiles", File.class); + cleanup.setAccessible(true); + cleanup.invoke(null, project.toFile()); + + assertFalse(Files.exists(sourceMarker)); + assertTrue(Files.isSymbolicLink(linkedMarker)); + assertEquals("package initializer\n", Files.readString(externalMarker)); + } + + @Test + void licensingDoesNotModifyFilesThroughSymlinks() throws Exception { + Path project = Files.createDirectory(directory.resolve("project")); + Path source = Files.createDirectories(project.resolve("src/example")); + Path external = Files.createDirectory(directory.resolve("external")); + Files.createSymbolicLink(project.resolve(".venv"), external); + Path externalJava = Files.writeString(external.resolve("External.java"), "class External {}\n"); + Path sourceJava = Files.writeString(source.resolve("Application.java"), "class Application {}\n"); + Files.createSymbolicLink(project.resolve("Linked.java"), externalJava); + + try (GuideProjectGenerator generator = new GuideProjectGenerator()) { + generator.addLicenses(project.toFile()); + } + + assertTrue(Files.readString(sourceJava).contains("Licensed under")); + assertEquals("class External {}\n", Files.readString(externalJava)); + } +} diff --git a/buildSrc/src/test/java/io/micronaut/guides/GuidesPluginTest.java b/buildSrc/src/test/java/io/micronaut/guides/GuidesPluginTest.java new file mode 100644 index 00000000000..a7ecbbad9fc --- /dev/null +++ b/buildSrc/src/test/java/io/micronaut/guides/GuidesPluginTest.java @@ -0,0 +1,78 @@ +package io.micronaut.guides; + +import io.micronaut.guides.tasks.AsciidocGenerationTask; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.function.Executable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertAll; + +class GuidesPluginTest { + + @TempDir + Path directory; + + @Test + void rendererIsOrderedAfterAllLanguageDocumentationTasks() throws IOException { + Project project = project(); + Task renderer = project.getTasks().getByName("asciidoctor"); + + assertAll(List.of("Java", "Groovy", "Kotlin", "Python").stream() + .map(language -> (Executable) () -> { + Task documentation = project.getTasks().getByName("helloGenerateDocs" + language); + assertTrue(renderer.getMustRunAfter().getDependencies(renderer).contains(documentation), + "Renderer must run after " + documentation.getName()); + })); + } + + @Test + void orderingDoesNotForceDocumentationGeneration() throws IOException { + Project project = project(); + Task renderer = project.getTasks().getByName("asciidoctor"); + Task documentation = project.getTasks().getByName("helloGenerateDocs"); + + assertTrue(renderer.getMustRunAfter().getDependencies(renderer).contains(documentation)); + assertTrue(renderer.getTaskDependencies().getDependencies(renderer).isEmpty(), + "Rendering existing documents must not schedule guide generation"); + } + + @Test + void orderingIncludesDocumentationTasksRegisteredLater() throws IOException { + Project project = project(); + Task renderer = project.getTasks().getByName("asciidoctor"); + Task documentation = project.getTasks().register("additionalDocs", AsciidocGenerationTask.class).get(); + + assertTrue(renderer.getMustRunAfter().getDependencies(renderer).contains(documentation)); + assertTrue(renderer.getTaskDependencies().getDependencies(renderer).isEmpty()); + } + + private Project project() throws IOException { + Path guide = Files.createDirectories(directory.resolve("guides/hello")); + Files.writeString(directory.resolve("guides/tests.properties"), "numberOfTestGroups=1\n"); + Files.writeString(guide.resolve("metadata.json"), """ + { + "title": "Hello", + "intro": "A task-wiring fixture", + "authors": ["Micronaut"], + "categories": ["Core"], + "publicationDate": "2026-10-05", + "languages": ["JAVA", "GROOVY", "KOTLIN", "PYTHON"], + "apps": [{"name": "default", "features": []}] + } + """); + Project project = ProjectBuilder.builder().withProjectDir(directory.toFile()).build(); + project.getExtensions().getExtraProperties().set("metadataConfigName", "metadata.json"); + project.getTasks().register("asciidoctor"); + project.getPluginManager().apply(GuidesPlugin.class); + return project; + } +} diff --git a/buildSrc/src/test/java/io/micronaut/guides/IndexGeneratorTest.java b/buildSrc/src/test/java/io/micronaut/guides/IndexGeneratorTest.java index 1a11c9b04f3..99fb4c8678e 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/IndexGeneratorTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/IndexGeneratorTest.java @@ -1,16 +1,128 @@ package io.micronaut.guides; +import io.micronaut.guides.core.App; +import io.micronaut.guides.core.Guide; +import io.micronaut.starter.api.TestFramework; +import io.micronaut.starter.options.BuildTool; +import io.micronaut.starter.options.Language; import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; class IndexGeneratorTest { + @TempDir + Path tempDir; + + @Test + void pyronautReadLinkPointsToPublishedGuidePage() throws IOException { + File template = tempDir.resolve("template.html").toFile(); + Files.writeString(template.toPath(), String.join("\n", + "", + "@title@@twittercard@", + "", + "
@breadcrumb@
", + "
@content@
", + "
@toccontent@
", + "", + "")); + + File distDir = tempDir.resolve("dist").toFile(); + assertTrue(distDir.mkdirs()); + + Guide guide = new Guide( + "Python Guide", + "A guide with a Pyronaut variant.", + List.of("Micronaut"), + List.of("Core Basics"), + LocalDate.of(2026, 5, 18), + null, + null, + null, + false, + false, + null, + List.of(Language.JAVA, Language.PYTHON), + List.of(), + List.of(BuildTool.GRADLE, BuildTool.PYRONAUT), + TestFramework.JUNIT, + List.of(), + "python-guide", + true, + null, + Map.of(), + List.of(new App("default", null, null, null, List.of(), null, null, null, null, null, null, null, true)), + false); + + IndexGenerator.generateGuidesIndex(template, distDir, List.of(guide), null); + + String html = Files.readString(distDir.toPath().resolve("python-guide.html")); + assertTrue(html.contains("python-guide-pyronaut-python.html'>Read")); + assertFalse(html.contains("python-guide.html'>Read")); + assertFalse(html.contains("python-guide-gradle-python.html")); + assertFalse(html.contains("python-guide-maven-python.html")); + assertTrue(html.matches("(?s).*
.*")); + } + + @Test + void jvmOnlyGuideDoesNotRenderPythonColumn() throws IOException { + File template = tempDir.resolve("jvm-template.html").toFile(); + Files.writeString(template.toPath(), String.join("\n", + "", + "@title@@twittercard@", + "", + "
@breadcrumb@
", + "
@content@
", + "
@toccontent@
", + "", + "")); + + File distDir = tempDir.resolve("jvm-dist").toFile(); + assertTrue(distDir.mkdirs()); + Guide guide = new Guide( + "JVM Guide", + "A JVM guide.", + List.of("Micronaut"), + List.of("Core Basics"), + LocalDate.of(2026, 5, 18), + null, + null, + null, + false, + false, + null, + List.of(Language.JAVA), + List.of(), + List.of(BuildTool.GRADLE), + TestFramework.JUNIT, + List.of(), + "jvm-guide", + true, + null, + Map.of(), + List.of(new App("default", null, null, null, List.of(), null, null, null, null, null, null, null, true)), + false); + + IndexGenerator.generateGuidesIndex(template, distDir, List.of(guide), null); + + String html = Files.readString(distDir.toPath().resolve("jvm-guide.html")); + assertFalse(html.contains("python.svg")); + assertFalse(html.contains("Pyronaut")); + } + @Test void pythonJsonContainsFlatPythonGuideEntries() throws Exception { String json = IndexGenerator.generatePythonJsonIndex( diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/AppTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/AppTest.java index d080db0041a..203acd40d52 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/AppTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/AppTest.java @@ -92,7 +92,7 @@ void buildGeneratesCorrectJsonSchema(ResourceLoader resourceLoader) throws IOExc String expected = """ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://micronaut-projects.github.io/micronaut-guides/schemas/app.schema.json", + "$id": "https://guides.micronaut.io/schemas/app.schema.json", "title": "App", "type": "object", "properties": { diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/DependencyMacroSubstitutionTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/DependencyMacroSubstitutionTest.java index c167dbebec5..f14bdb0f4bc 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/DependencyMacroSubstitutionTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/DependencyMacroSubstitutionTest.java @@ -69,7 +69,7 @@ void testSubstitute(){ } @Test - void pythonDependencyMacrosAreOmitted() { + void pythonDependencyMacrosUsePyprojectToml() { String source = """ Before @@ -87,7 +87,14 @@ void pythonDependencyMacrosAreOmitted() { assertEquals(""" Before - + [source, toml] + .pyproject.toml + ---- + [tool.pyronaut.dependencies] + runtime = [ + "io.micronaut.views:micronaut-views-thymeleaf", + ] + ---- After """, result); diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/FilesTransferUtilityTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/FilesTransferUtilityTest.java index a0353eadbcb..cc3481979fd 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/FilesTransferUtilityTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/FilesTransferUtilityTest.java @@ -3,12 +3,15 @@ import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.File; import java.util.ArrayList; import java.util.List; import java.nio.file.Files; +import java.nio.file.Path; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -99,4 +102,24 @@ void pythonViewsAreCopiedToConfig() throws Exception { assertFalse(new File(destination, "src/main/resources/static/python.txt").exists()); } + @Test + void pythonUsesApplicationConfigurationWithoutInheritingBootstrapFiles(@TempDir Path output) throws Exception { + File inputDirectory = new File("src/test/resources/file-transfer/python-application-config"); + Guide guide = guideParser.parseGuideMetadata(inputDirectory, "metadata.json").orElseThrow(); + + filesTransferUtility.transferFiles(inputDirectory, output.toFile(), guide); + + Path python = output.resolve("python-application-config-pyronaut-python"); + assertEquals("[micronaut.application]\nname = \"python-application-config\"\n", + Files.readString(python.resolve("config/application.toml"))); + assertEquals("[micronaut.config]\nimport = []\n", + Files.readString(python.resolve("tests-config/application-test.toml"))); + assertFalse(Files.exists(python.resolve("config/bootstrap.properties"))); + assertFalse(Files.exists(python.resolve("tests-config/bootstrap-test.properties"))); + + Path java = output.resolve("python-application-config-gradle-java"); + assertTrue(Files.exists(java.resolve("src/main/resources/bootstrap.properties"))); + assertTrue(Files.exists(java.resolve("src/test/resources/bootstrap-test.properties"))); + } + } diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/GuideGenerationUtilsTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/GuideGenerationUtilsTest.java index ce394a7bd43..eea2d1e659e 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/GuideGenerationUtilsTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/GuideGenerationUtilsTest.java @@ -81,6 +81,38 @@ void testGroovyGuideProjectGeneratorGuidesOptions() { .anyMatch(option -> option.getBuildTool() == BuildTool.MAVEN && option.getLanguage() == Language.KOTLIN)); } + @Test + void pythonGuideOptionsUsePytestEvenWhenJvmGuideUsesJunit() { + Guide guideMetadata = new Guide(null,null, null, null, null, null, null, null,false,false,null, List.of(Language.JAVA, Language.PYTHON),null, List.of(BuildTool.GRADLE, BuildTool.PYRONAUT), TestFramework.JUNIT,null,null,true,null,null,null); + + List result = GuideGenerationUtils.guidesOptions(guideMetadata, LOG); + + assertTrue(result.stream() + .anyMatch(option -> option.getBuildTool() == BuildTool.GRADLE && + option.getLanguage() == Language.JAVA && + option.getTestFramework() == TestFramework.JUNIT)); + assertTrue(result.stream() + .anyMatch(option -> option.getBuildTool() == BuildTool.PYRONAUT && + option.getLanguage() == Language.PYTHON && + option.getTestFramework() == TestFramework.PYTEST)); + } + + @Test + void groovyGuideProjectGeneratorPythonOptionsUsePytestEvenWhenJvmGuideUsesJunit() { + Guide guideMetadata = new Guide(null,null, null, null, null, null, null, null,false,false,null, List.of(Language.JAVA, Language.PYTHON),null, List.of(BuildTool.GRADLE, BuildTool.PYRONAUT), TestFramework.JUNIT,null,null,true,null,null,null); + + List result = io.micronaut.guides.GuideProjectGenerator.guidesOptions(guideMetadata); + + assertTrue(result.stream() + .anyMatch(option -> option.getBuildTool() == BuildTool.GRADLE && + option.getLanguage() == Language.JAVA && + option.getTestFramework() == TestFramework.JUNIT)); + assertTrue(result.stream() + .anyMatch(option -> option.getBuildTool() == BuildTool.PYRONAUT && + option.getLanguage() == Language.PYTHON && + option.getTestFramework() == TestFramework.PYTEST)); + } + @Test void testGroovyPythonGuideOptionsUsePyronautAndPytest() { Guide guideMetadata = new Guide(null,null, null, null, null, null, null, null,false,false,null, List.of(Language.JAVA, Language.PYTHON),null, List.of(BuildTool.GRADLE, BuildTool.MAVEN), TestFramework.SPOCK,null,null,true,null,null,null); @@ -103,6 +135,7 @@ void testTestFrameworkOption() { assertEquals(TestFramework.PYTEST, GuideGenerationUtils.testFrameworkOption(Language.PYTHON, TestFramework.SPOCK)); assertEquals(TestFramework.JUNIT, GuideGenerationUtils.testFrameworkOption(Language.JAVA, null)); assertEquals(TestFramework.SPOCK, GuideGenerationUtils.testFrameworkOption(Language.JAVA, TestFramework.SPOCK)); + assertEquals(TestFramework.PYTEST, GuideGenerationUtils.testFrameworkOption(Language.PYTHON, TestFramework.JUNIT)); } @Test diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java index e4481630233..24ae18a3ff7 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java @@ -39,8 +39,10 @@ void testParseGuidesMetadata() { assertEquals("child",guide.slug()); assertEquals(List.of(Language.JAVA, Language.GROOVY, Language.KOTLIN),guide.languages()); assertEquals(List.of(BuildTool.GRADLE, BuildTool.MAVEN),guide.buildTools()); + assertFalse(guide.languages().contains(Language.PYTHON)); assertTrue(guide.zipIncludes().isEmpty()); assertTrue(guide.env().isEmpty()); + assertFalse(guideParser.parseGuideMetadata(new File(path, "child"), "metadata.json").orElseThrow().languages().contains(Language.PYTHON)); List tags = guide.tags(); Collections.sort(tags); assertEquals(List.of("Azure", "cloud", "data-jdbc", "database", "flyway", "jdbc", "micronaut-data", "mysql"), tags); diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectGeneratorTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectGeneratorTest.java index 09d06759bf7..a19693bab5b 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectGeneratorTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectGeneratorTest.java @@ -8,14 +8,17 @@ import jakarta.inject.Inject; import org.gradle.api.JavaVersion; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.File; import java.nio.file.Paths; +import java.nio.file.Path; import java.time.LocalDate; import java.util.List; import java.util.Map; import static io.micronaut.guides.core.TestUtils.readFile; +import static io.micronaut.starter.options.JdkVersion.JDK_25; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -215,5 +218,28 @@ void testGenerateOracleAtpMavenProjectWithOracleFreeTestResources() { assertTrue(result.contains("micronaut-test-resources-jdbc-oracle-free"), result); } + @Test + void testGenerateOracleAtpPythonProjectWithH2Runtime(@TempDir Path outputDirectory) throws Exception { + App app = new App( + "default", "example.micronaut", ApplicationType.DEFAULT, "Micronaut", + List.of("data-jdbc", "flyway", "oracle-cloud-atp"), + List.of(), List.of(), List.of(), List.of(), null, null, null, null, false, + List.of("h2-runtime") + ); + Guide guide = GuideTestUtils.guideWithSlug("oracle-atp-h2-runtime"); + GuidesOption option = new GuidesOption(BuildTool.PYRONAUT, Language.PYTHON, TestFramework.PYTEST); + + ((DefaultGuideProjectGenerator) guideProjectGenerator).generate( + outputDirectory.toFile(), guide, option, JDK_25, app); + + File dest = outputDirectory.resolve(MacroUtils.getSourceDir(guide.slug(), option)).toFile(); + String result = readFile(new File(dest, "pyproject.toml")); + String runtime = result.substring(result.indexOf("runtime = ["), result.indexOf("build = [")); + assertTrue(runtime.contains("\"com.h2database:h2\""), result); + assertTrue(runtime.contains("\"io.micronaut.oraclecloud:micronaut-oraclecloud-atp\""), result); + String configuration = readFile(new File(dest, "config/application.toml")); + assertTrue(configuration.contains("dialect = 'ORACLE'"), configuration); + } + } diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectZipperTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectZipperTest.java index 3a4a109a732..16289c966f5 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectZipperTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectZipperTest.java @@ -26,7 +26,7 @@ void testZip() throws IOException { guideProjectZipper.zipDirectory(projectFolder, zipFile); - List expected = List.of("metadata.json", "creating-your-first-micronaut-app.adoc"); + List expected = List.of("creating-your-first-micronaut-app.adoc", "metadata.json"); List result = new LinkedList<>(); try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { @@ -38,6 +38,7 @@ void testZip() throws IOException { zis.closeEntry(); } + result.sort(String::compareTo); assertEquals(expected, result); } } diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/GuideTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/GuideTest.java index 246511e826d..4ee8579b058 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/GuideTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/GuideTest.java @@ -105,6 +105,15 @@ void typeCloudCanBeNull() { assertTrue(violations.isEmpty()); } + @Test + void appsMustNotBeNull() { + Guide guide = new Guide("Documentation guide", "Connect the existing application.", + List.of("Micronaut"), List.of("Data JDBC"), LocalDate.of(2026, 10, 6), + null, null, null, false, false, null, null, null, null, null, null, null, + true, null, null, null); + assertTrue(validator.validate(guide).stream().anyMatch(violation -> violation.getPropertyPath().toString().equals("apps"))); + } + @Test void testDeserialization() { Optional inputStreamOptional = resourceLoader.getResourceAsStream("classpath:metadata.json"); @@ -221,17 +230,16 @@ void buildGeneratesCorrectJsonSchema(ResourceLoader resourceLoader) throws IOExc String expected = """ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://micronaut-projects.github.io/micronaut-guides/schemas/guide.schema.json", + "$id": "https://guides.micronaut.io/schemas/guide.schema.json", "title": "Guide", "type": "object", "properties": { "apps": { - "description": "Applications created for the guide", + "description": "Applications created for the guide; an empty list denotes a documentation-only guide with no generated application", "type": "array", "items": { - "$ref": "https://micronaut-projects.github.io/micronaut-guides/schemas/app.schema.json" - }, - "minItems": 1 + "$ref": "https://guides.micronaut.io/schemas/app.schema.json" + } }, "asciidoctor": { "description": "The guide asciidoc file. If not specified, the guide slug followed by the .adoc suffix is used", diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/GuideUtilsTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/GuideUtilsTest.java index 164068c8fcf..21acc05ce4d 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/GuideUtilsTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/GuideUtilsTest.java @@ -9,8 +9,10 @@ import org.junit.jupiter.api.Test; import java.io.InputStream; +import java.time.LocalDate; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -185,4 +187,41 @@ void testGetFrameworks() { Set expected = Set.of("Spring Boot","Micronaut"); assertEquals(expected,frameworks); } + + @Test + void testMergePreservesPythonOptionsFromBase() { + App baseApp = new App("default", null, null, null, List.of(), List.of(), List.of(), List.of(), List.of(), List.of("base-jvm"), null, null, null, true, List.of("base")); + Guide base = new Guide("base", "base", List.of("author"), List.of("category"), LocalDate.of(2026, 1, 1), null, null, null, + false, false, null, List.of(Language.JAVA, Language.GROOVY, Language.KOTLIN, Language.PYTHON), List.of(), + List.of(BuildTool.GRADLE, BuildTool.MAVEN, BuildTool.PYRONAUT), null, List.of(), "base", true, null, Map.of(), List.of(baseApp), false); + App childApp = new App("default", null, null, null, List.of(), List.of(), List.of(), List.of(), List.of(), List.of("child-jvm"), null, null, null, true, List.of("child")); + Guide child = new Guide("child", "child", List.of(), List.of("category"), LocalDate.of(2026, 1, 1), null, null, null, + false, false, null, null, List.of(), + null, null, List.of(), "child", true, "base", Map.of(), List.of(childApp), false); + + Guide merged = GuideUtils.merge(base, child); + + assertTrue(merged.languages().contains(Language.PYTHON)); + assertTrue(merged.buildTools().contains(BuildTool.PYRONAUT)); + assertEquals(List.of("child", "base"), merged.apps().get(0).pythonFeatures()); + assertEquals(List.of("child-jvm", "base-jvm"), merged.apps().get(0).jvmFeatures()); + assertEquals(List.of("child", "base"), GuideUtils.getAppVisibleFeatures(merged.apps().get(0), Language.PYTHON)); + assertEquals(List.of("child-jvm", "base-jvm"), GuideUtils.getAppVisibleFeatures(merged.apps().get(0), Language.JAVA)); + } + + @Test + void testMergeAllowsChildToDisablePython() { + App app = new App("default", null, null, null, List.of(), List.of(), List.of(), List.of(), List.of(), null, null, null, null, true, List.of()); + Guide base = new Guide("base", "base", List.of("author"), List.of("category"), LocalDate.of(2026, 1, 1), null, null, null, + false, false, null, List.of(Language.JAVA, Language.GROOVY, Language.KOTLIN, Language.PYTHON), List.of(), + List.of(BuildTool.GRADLE, BuildTool.MAVEN, BuildTool.PYRONAUT), null, List.of(), "base", true, null, Map.of(), List.of(app), false); + Guide child = new Guide("child", "child", List.of(), List.of("category"), LocalDate.of(2026, 1, 1), null, null, null, + false, false, null, List.of(Language.JAVA, Language.GROOVY, Language.KOTLIN), List.of(), + List.of(BuildTool.GRADLE, BuildTool.MAVEN), null, List.of(), "child", true, "base", Map.of(), List.of(app), false); + + Guide merged = GuideUtils.merge(base, child); + + assertFalse(merged.languages().contains(Language.PYTHON)); + assertFalse(merged.buildTools().contains(BuildTool.PYRONAUT)); + } } diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/MacroUtilsTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/MacroUtilsTest.java index 7a4de186665..3c24f89255e 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/MacroUtilsTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/MacroUtilsTest.java @@ -22,6 +22,11 @@ void testGetSourceDir() { assertEquals("slug-gradle-java", result); } + @Test + void testPythonModuleNameNormalizesMixedSnakeCase() { + assertEquals("hello_controller", MacroUtils.pythonModuleName("Hello_Controller")); + } + @Test void testFindMacroGroups() { String str = """ diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/SourceMacroSubstitutionTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/SourceMacroSubstitutionTest.java index 0fb1326173c..e4ef42a8b2c 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/SourceMacroSubstitutionTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/SourceMacroSubstitutionTest.java @@ -46,6 +46,20 @@ void testSubstitute(){ assertEquals(expectedGroovy, resGroovy); } + @Test + void testSubstitutePyronautPythonKeepsSourceCallouts() { + String str = "source:HelloController[]\n"; + String resPython = sourceMacroSubstitution.substitute(str, GuideTestUtils.guideWithSlug("creating-your-first-micronaut-app"), new GuidesOption(BuildTool.PYRONAUT, Language.PYTHON, TestFramework.PYTEST)); + String expectedPython = """ + [source,python] + .src/example/micronaut/hello_controller.py + ---- + include::{sourceDir}/creating-your-first-micronaut-app/creating-your-first-micronaut-app-pyronaut-python/src/example/micronaut/hello_controller.py[] + ---- + """; + assertEquals(expectedPython, resPython); + } + @Test void TestSubstituteWithApp(){ String str = "source:Application[app=springboot]\n"; diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/TestMacroSubstitutionTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/TestMacroSubstitutionTest.java index 4d4a2c8f263..95207ab110f 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/TestMacroSubstitutionTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/TestMacroSubstitutionTest.java @@ -44,6 +44,19 @@ void testSubstitute() { assertEquals(expectedGroovy, resGroovy); } + @Test + void testSubstitutePyronautPythonKeepsTestCallouts() { + String str = "test:HelloControllerTest[]"; + String resPython = testMacroSubstitution.substitute(str, GuideTestUtils.guideWithSlug("creating-your-first-micronaut-app"), new GuidesOption(BuildTool.PYRONAUT, Language.PYTHON, TestFramework.PYTEST)); + String expectedPython = """ + [source,python] + .tests/example/micronaut/test_hello_controller.py + ---- + include::{sourceDir}/creating-your-first-micronaut-app/creating-your-first-micronaut-app-pyronaut-python/tests/example/micronaut/test_hello_controller.py[] + ----"""; + assertEquals(expectedPython, resPython); + } + @Test void TestSubstituteWithApp() { String str = "test:ApplicationTest[app=springboot]"; diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/TestScriptGeneratorTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/TestScriptGeneratorTest.java index d55ba17fa4d..4ef0572f49a 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/TestScriptGeneratorTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/TestScriptGeneratorTest.java @@ -13,6 +13,7 @@ import static io.micronaut.starter.options.BuildTool.GRADLE; import static io.micronaut.starter.options.BuildTool.MAVEN; +import static io.micronaut.starter.options.BuildTool.PYRONAUT; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -119,6 +120,72 @@ void testGenerate() { assertEquals(expected.strip(), result.strip()); } + @Test + void testGeneratePython() { + App app = new App("default", null, null, null, null, null, null, null, null, null, null, null, true); + Guide guide = new Guide( + "Python guide", + "Tests Python guide script generation.", + List.of("Micronaut"), + List.of("Getting Started"), + java.time.LocalDate.of(2026, 5, 13), + null, + null, + null, + false, + false, + null, + List.of(Language.PYTHON), + List.of(), + List.of(PYRONAUT), + TestFramework.PYTEST, + List.of(), + "python-guide", + true, + null, + java.util.Map.of(), + List.of(app), + false + ); + + String result = testScriptGenerator.generatePythonTestScript(new ArrayList<>(List.of(guide))); + + assertTrue(result.contains("PYRONAUT_LOCAL_REPOSITORY")); + assertTrue(result.contains("PYRONAUT_LOCAL_CORE_VERSION")); + assertTrue(result.contains("PYRONAUT_LOCAL_PLATFORM_VERSION")); + assertTrue(result.contains("PYRONAUT_INSTALL_EXECUTABLE")); + assertTrue(result.contains("PYRONAUT_REFRESH_DEPENDENCIES")); + assertTrue(result.contains("PYRONAUT_DEPENDENCY_CACHE:-.pyronaut-m2")); + assertTrue(result.contains("PYRONAUT_PROCESS_EXECUTABLE")); + assertTrue(result.contains("PYRONAUT_CLI_PYTHONPATH")); + assertTrue(result.contains("set_pyronaut_local_versions")); + int runPyronautTests = result.indexOf("run_pyronaut_tests () {"); + assertTrue(runPyronautTests >= 0); + int directBranch = result.indexOf("if [ -z \"${PYRONAUT_LOCAL_REPOSITORY:-}\" ]; then", runPyronautTests); + int tempBranch = result.indexOf("local tmp_dir", directBranch); + String directBranchScript = result.substring(directBranch, tempBranch); + int localVersionsInDirectBranch = directBranchScript.indexOf("set_pyronaut_local_versions"); + int testResourcesInDirectBranch = directBranchScript.indexOf("set_pyronaut_test_resources"); + assertTrue(localVersionsInDirectBranch >= 0 + && localVersionsInDirectBranch < testResourcesInDirectBranch); + assertTrue(result.contains("[tool.pyronaut.core]")); + assertTrue(result.contains("[tool.pyronaut.platform]")); + assertFalse(result.contains("PYRONAUT_LOCAL_VERSION")); + assertTrue(result.contains("run_pyronaut_install")); + assertTrue(result.contains("run_pyronaut_process")); + assertTrue(result.contains("run_pyronaut_cli")); + assertTrue(result.contains("test_args+=(--local-repository \"$(pyronaut_dependency_cache)\")")); + assertTrue(result.contains("run_pyronaut_cli test \"${test_args[@]}\"")); + assertFalse(result.contains("\"$PYRONAUT_TEST_EXECUTABLE\"")); + assertTrue(result.contains("set_pyronaut_local_repositories")); + assertTrue(result.contains("cd python-guide-pyronaut-python")); + assertFalse(result.contains("}cd python-guide-pyronaut-python")); + assertTrue(result.contains("run_pyronaut_tests || EXIT_STATUS=$?")); + assertFalse(result.contains("pyenv")); + assertTrue(result.contains("tar --exclude='./.micronaut'")); + assertFalse(result.contains("./gradlew -q check")); + } + @Test void testGenerateNative() { String path = "src/test/resources/guides"; @@ -134,17 +201,17 @@ void testGenerateNative() { } @Test - void testGeneratePython() { + void testGenerateGenericScriptSkipsPython() { File guideFolder = new File("src/test/resources/file-transfer/python-resources"); Guide guide = guideParser.parseGuideMetadata(guideFolder, "metadata.json").orElseThrow(); String result = testScriptGenerator.generateTestScript(new ArrayList<>(List.of(guide))); - assertTrue(result.contains("cd python-resources-pyronaut-python")); + assertFalse(result.contains("python-resources-pyronaut-python")); assertFalse(result.contains("pyenv")); - assertTrue(result.contains("pyronaut install")); - assertTrue(result.contains("pyronaut validate-config")); - assertTrue(result.contains("pyronaut test")); + assertFalse(result.contains("pyronaut install")); + assertFalse(result.contains("pyronaut validate-config")); + assertFalse(result.contains("pyronaut test")); assertFalse(result.contains("./gradlew -q check")); assertFalse(result.contains("./mvnw -q test")); } diff --git a/buildSrc/src/test/java/io/micronaut/guides/tasks/PythonTestScriptTaskTest.java b/buildSrc/src/test/java/io/micronaut/guides/tasks/PythonTestScriptTaskTest.java new file mode 100644 index 00000000000..3e5891e011a --- /dev/null +++ b/buildSrc/src/test/java/io/micronaut/guides/tasks/PythonTestScriptTaskTest.java @@ -0,0 +1,28 @@ +package io.micronaut.guides.tasks; + +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PythonTestScriptTaskTest { + + @Test + void pyronautTestFunctionsResourceIsAnInput() { + Method property = Arrays.stream(PythonTestScriptTask.class.getMethods()) + .filter(method -> method.getName().equals("getPyronautTestFunctionsFile")) + .findFirst() + .orElse(null); + + assertNotNull(property, "The helper resource must be a task property"); + assertTrue(property.isAnnotationPresent(InputFile.class)); + assertEquals(PathSensitivity.RELATIVE, property.getAnnotation(PathSensitive.class).value()); + } +} diff --git a/buildSrc/src/test/java/io/micronaut/guides/tasks/SampleProjectGenerationTaskTest.java b/buildSrc/src/test/java/io/micronaut/guides/tasks/SampleProjectGenerationTaskTest.java new file mode 100644 index 00000000000..c74d98a0e6a --- /dev/null +++ b/buildSrc/src/test/java/io/micronaut/guides/tasks/SampleProjectGenerationTaskTest.java @@ -0,0 +1,140 @@ +package io.micronaut.guides.tasks; + +import io.micronaut.guides.GuideProjectGenerator; +import io.micronaut.guides.core.App; +import io.micronaut.guides.core.Guide; +import io.micronaut.guides.core.App; +import io.micronaut.starter.api.TestFramework; +import io.micronaut.starter.application.ApplicationType; +import io.micronaut.starter.options.BuildTool; +import io.micronaut.starter.options.Language; +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SampleProjectGenerationTaskTest { + + @TempDir + Path directory; + + @Test + void outputDirectoriesAreLimitedToTheRequestedLanguage() { + Guide metadata = new Guide(null, null, null, null, null, null, null, null, + false, false, null, List.of(Language.JAVA, Language.PYTHON), null, + List.of(BuildTool.GRADLE, BuildTool.MAVEN), TestFramework.JUNIT, + null, "multi-language", true, null, null, + List.of(new App("default", null, null, null, null, null, null, null, null, null, null, null, false))); + + assertEquals(List.of("multi-language-pyronaut-python"), + SampleProjectGenerationTask.outputDirectoryNames(metadata, Language.PYTHON)); + assertEquals(List.of("multi-language-gradle-java", "multi-language-maven-java", + "multi-language-pyronaut-python"), + SampleProjectGenerationTask.outputDirectoryNames(metadata, null)); + } + + @Test + void pythonRegenerationRefreshesProjectAndPreservesOtherLanguages() throws IOException { + Path input = Files.createDirectories(directory.resolve("guide")); + Path output = directory.resolve("output"); + Path obsoleteSource = write(input.resolve("python/src/obsolete.py"), "obsolete = True\n"); + Guide metadata = metadata(List.of()); + + try (GuideProjectGenerator generator = new GuideProjectGenerator()) { + SampleProjectGenerationTask task = task(metadata, input, output, Language.PYTHON, generator); + task.perform(); + Path pythonProject = output.resolve("regeneration-pyronaut-python"); + Path pyproject = pythonProject.resolve("pyproject.toml"); + assertFalse(Files.readString(pyproject).contains("io.micronaut.validation:micronaut-validation")); + assertTrue(Files.exists(pythonProject.resolve("src/obsolete.py"))); + + Path javaSentinel = write(output.resolve("regeneration-gradle-java/sentinel.txt"), "Java survives\n"); + Path kotlinSentinel = write(output.resolve("regeneration-gradle-kotlin/sentinel.txt"), "Kotlin survives\n"); + Files.delete(obsoleteSource); + write(input.resolve("python/src/current.py"), "current = True\n"); + task.setMetadata(metadata(List.of("validation"))); + + task.perform(); + + String regeneratedPyproject = Files.readString(pyproject); + assertAll( + () -> assertTrue(regeneratedPyproject.contains("io.micronaut.validation:micronaut-validation"), + "The changed Starter feature must update pyproject.toml dependencies"), + () -> assertFalse(Files.exists(pythonProject.resolve("src/obsolete.py")), + "Removed guide samples must not survive regeneration"), + () -> assertEquals("current = True\n", Files.readString(pythonProject.resolve("src/current.py"))), + () -> assertEquals("Java survives\n", Files.readString(javaSentinel)), + () -> assertEquals("Kotlin survives\n", Files.readString(kotlinSentinel))); + } + } + + @Test + void unfilteredGenerationCleansTheEntireOutputDirectory() throws IOException { + Path input = Files.createDirectories(directory.resolve("guide")); + Path output = directory.resolve("output"); + Path stalePython = write(output.resolve("regeneration-pyronaut-python/stale.txt"), "stale\n"); + Path staleJava = write(output.resolve("regeneration-gradle-java/stale.txt"), "stale\n"); + Path staleKotlin = write(output.resolve("regeneration-gradle-kotlin/stale.txt"), "stale\n"); + Path undeclaredOutput = write(output.resolve("obsolete-project/stale.txt"), "stale\n"); + + try (GuideProjectGenerator generator = new GuideProjectGenerator()) { + task(metadata(List.of("validation")), input, output, null, generator).perform(); + + assertAll( + () -> assertFalse(Files.exists(stalePython)), + () -> assertFalse(Files.exists(staleJava)), + () -> assertFalse(Files.exists(staleKotlin)), + () -> assertFalse(Files.exists(undeclaredOutput)), + () -> assertTrue(Files.exists(output.resolve("regeneration-pyronaut-python/pyproject.toml"))), + () -> assertTrue(Files.readString(output.resolve("regeneration-pyronaut-python/pyproject.toml")) + .contains("io.micronaut.validation:micronaut-validation")), + () -> assertTrue(Files.exists(output.resolve("regeneration-gradle-java/build.gradle"))), + () -> assertTrue(Files.exists(output.resolve("regeneration-gradle-kotlin/build.gradle")))); + } + } + + private SampleProjectGenerationTask task(Guide metadata, Path input, Path output, Language language, + GuideProjectGenerator generator) throws IOException { + Project project = ProjectBuilder.builder() + .withProjectDir(Files.createDirectories(directory.resolve("gradle-project")).toFile()) + .build(); + SampleProjectGenerationTask task = project.getTasks().create("generateProjects", SampleProjectGenerationTask.class); + task.setMetadata(metadata); + task.setGuidesGenerator(generator); + task.getSlug().set(metadata.slug()); + task.getInputDirectory().set(input.toFile()); + task.getOutputDir().set(output.toFile()); + if (language != null) { + task.getLanguage().set(language.name()); + } + task.getOutputDirectories().from(SampleProjectGenerationTask.outputDirectoryNames(metadata, language) + .stream().map(name -> output.resolve(name).toFile()).toList()); + return task; + } + + private static Guide metadata(List features) { + App app = new App("default", "example.micronaut", ApplicationType.DEFAULT, "Micronaut", + features, List.of(), List.of(), List.of(), List.of(), null, null, null, false); + return new Guide("Regeneration", "Sample project regeneration", List.of("author"), List.of("Testing"), + LocalDate.of(2026, 10, 6), null, null, null, false, false, null, + List.of(Language.JAVA, Language.KOTLIN, Language.PYTHON), List.of(), List.of(BuildTool.GRADLE), + TestFramework.JUNIT, List.of(), "regeneration", true, null, Map.of(), List.of(app)); + } + + private static Path write(Path path, String content) throws IOException { + Files.createDirectories(path.getParent()); + return Files.writeString(path, content); + } +} diff --git a/buildSrc/src/test/resources/expected_test_script.sh b/buildSrc/src/test/resources/expected_test_script.sh index 79c79f14255..891c08c16d7 100644 --- a/buildSrc/src/test/resources/expected_test_script.sh +++ b/buildSrc/src/test/resources/expected_test_script.sh @@ -158,22 +158,6 @@ if [ $EXIT_STATUS -ne 0 ]; then fi EXIT_STATUS=0 -cd hello-base-gradle-java - -cd .. -cd hello-base-gradle-groovy - -cd .. -cd hello-base-gradle-kotlin - -cd .. -cd hello-base-maven-java - -cd .. -cd hello-base-maven-groovy - -cd .. - cd test-gradle-java cd springboot echo "-------------------------------------------------" diff --git a/buildSrc/src/test/resources/expected_test_script_native.sh b/buildSrc/src/test/resources/expected_test_script_native.sh index a8229d59e46..086a5802775 100644 --- a/buildSrc/src/test/resources/expected_test_script_native.sh +++ b/buildSrc/src/test/resources/expected_test_script_native.sh @@ -74,22 +74,6 @@ if [ $EXIT_STATUS -ne 0 ]; then fi EXIT_STATUS=0 -cd hello-base-gradle-java - -cd .. -cd hello-base-gradle-groovy - -cd .. -cd hello-base-gradle-kotlin - -cd .. -cd hello-base-maven-java - -cd .. -cd hello-base-maven-groovy - -cd .. - cd test-gradle-java cd micronautframeworkjacksondatabind echo "-------------------------------------------------" diff --git a/buildSrc/src/test/resources/file-transfer/python-application-config/metadata.json b/buildSrc/src/test/resources/file-transfer/python-application-config/metadata.json new file mode 100644 index 00000000000..5f75339abcc --- /dev/null +++ b/buildSrc/src/test/resources/file-transfer/python-application-config/metadata.json @@ -0,0 +1,10 @@ +{ + "title": "Python application configuration", + "intro": "Test Python application configuration without inherited bootstrap files.", + "authors": ["Micronaut"], + "categories": ["Getting Started"], + "publicationDate": "2026-01-01", + "languages": ["JAVA", "PYTHON"], + "buildTools": ["GRADLE", "PYRONAUT"], + "apps": [{"name": "default"}] +} diff --git a/buildSrc/src/test/resources/file-transfer/python-application-config/python/config/application.toml b/buildSrc/src/test/resources/file-transfer/python-application-config/python/config/application.toml new file mode 100644 index 00000000000..7dff7b04978 --- /dev/null +++ b/buildSrc/src/test/resources/file-transfer/python-application-config/python/config/application.toml @@ -0,0 +1,2 @@ +[micronaut.application] +name = "python-application-config" diff --git a/buildSrc/src/test/resources/file-transfer/python-application-config/python/tests-config/application-test.toml b/buildSrc/src/test/resources/file-transfer/python-application-config/python/tests-config/application-test.toml new file mode 100644 index 00000000000..a31d948c365 --- /dev/null +++ b/buildSrc/src/test/resources/file-transfer/python-application-config/python/tests-config/application-test.toml @@ -0,0 +1,2 @@ +[micronaut.config] +import = [] diff --git a/buildSrc/src/test/resources/file-transfer/python-application-config/src/main/resources/bootstrap.properties b/buildSrc/src/test/resources/file-transfer/python-application-config/src/main/resources/bootstrap.properties new file mode 100644 index 00000000000..682df479809 --- /dev/null +++ b/buildSrc/src/test/resources/file-transfer/python-application-config/src/main/resources/bootstrap.properties @@ -0,0 +1 @@ +micronaut.config-client.enabled=true diff --git a/buildSrc/src/test/resources/file-transfer/python-application-config/src/test/resources/bootstrap-test.properties b/buildSrc/src/test/resources/file-transfer/python-application-config/src/test/resources/bootstrap-test.properties new file mode 100644 index 00000000000..8e850422cc3 --- /dev/null +++ b/buildSrc/src/test/resources/file-transfer/python-application-config/src/test/resources/bootstrap-test.properties @@ -0,0 +1 @@ +micronaut.config-client.enabled=false diff --git a/gradle/local-composite-builds.gradle b/gradle/local-composite-builds.gradle new file mode 100644 index 00000000000..c4d419495f2 --- /dev/null +++ b/gradle/local-composite-builds.gradle @@ -0,0 +1,151 @@ +def micronautCoreModules = [ + 'micronaut-aop', + 'micronaut-buffer-netty', + 'micronaut-context', + 'micronaut-context-propagation', + 'micronaut-context-python', + 'micronaut-core', + 'micronaut-core-bom', + 'micronaut-core-processor', + 'micronaut-core-reactive', + 'micronaut-discovery-core', + 'micronaut-function', + 'micronaut-function-client', + 'micronaut-function-web', + 'micronaut-graal', + 'micronaut-http', + 'micronaut-http-client', + 'micronaut-http-client-core', + 'micronaut-http-client-jdk', + 'micronaut-http-netty', + 'micronaut-http-netty-http3', + 'micronaut-http-server', + 'micronaut-http-server-netty', + 'micronaut-http-validation', + 'micronaut-inject', + 'micronaut-inject-groovy', + 'micronaut-inject-java', + 'micronaut-inject-kotlin', + 'micronaut-inject-python', + 'micronaut-jackson-core', + 'micronaut-jackson-databind', + 'micronaut-json-core', + 'micronaut-management', + 'micronaut-messaging', + 'micronaut-module-info', + 'micronaut-module-info-runtime', + 'micronaut-retry', + 'micronaut-router', + 'micronaut-runtime', + 'micronaut-runtime-osx', + 'micronaut-websocket' +] + +def micronautStarterModules = [ + 'micronaut-starter-core': ':starter-core', + 'micronaut-starter-api': ':starter-api', + 'micronaut-starter-aws-cdk': ':starter-aws-cdk', + 'micronaut-starter-aws-lambda': ':starter-aws-lambda', + 'micronaut-starter-azure-function': ':starter-azure-function', + 'micronaut-starter-gcp-function': ':starter-gcp-function', + 'micronaut-starter-web-netty': ':starter-web-netty', + 'micronaut-starter-web-servlet': ':starter-web-servlet', + 'micronaut-cli': ':micronaut-cli' +] + +def pyronautModules = [ + 'micronaut-pyronaut': ':micronaut-pyronaut', + 'micronaut-pyronaut-bom': ':micronaut-pyronaut-bom', + 'micronaut-pyronaut-config-model': ':micronaut-pyronaut-config-model', + 'micronaut-pyronaut-install': ':micronaut-pyronaut-install', + 'micronaut-pyronaut-logging': ':micronaut-pyronaut-logging', + 'micronaut-pyronaut-logback': ':micronaut-pyronaut-logback', + 'micronaut-pyronaut-native-build': ':micronaut-pyronaut-native-build', + 'micronaut-pyronaut-processor': ':micronaut-pyronaut-processor', + 'micronaut-pyronaut-pytest': ':micronaut-pyronaut-pytest', + 'micronaut-pyronaut-requests': ':micronaut-pyronaut-requests', + 'micronaut-pyronaut-run': ':micronaut-pyronaut-run', + 'micronaut-pyronaut-test': ':micronaut-pyronaut-test', + 'micronaut-pyronaut-test-resources-server': ':micronaut-pyronaut-test-resources-server', + 'micronaut-pyronaut-tui': ':micronaut-pyronaut-tui', + 'micronaut-pyronaut-validate-config': ':micronaut-pyronaut-validate-config' +] + +def localGitPath = { String propertyName, String environmentName -> + providers.gradleProperty(propertyName).orNull ?: providers.environmentVariable(environmentName).orNull +} + +def discoverGraalPyBundleRepository = { File checkoutDir -> + File bundleRoot = new File(checkoutDir, 'checkouts/maven-bundles/graalpy') + if (!bundleRoot.isDirectory()) { + return null + } + List candidates = bundleRoot.listFiles() + ?.findAll { File file -> + file.isDirectory() && + new File(file, 'org/graalvm/python/python').exists() && + new File(file, 'org/graalvm/python/python-embedding').exists() + } + ?.sort { File left, File right -> right.lastModified() <=> left.lastModified() } ?: [] + candidates.isEmpty() ? null : candidates.first().absolutePath +} + +def isUsableGraalPyBundleRepository = { String repository -> + repository != null && + new File(repository, 'org/graalvm/python/python').exists() && + new File(repository, 'org/graalvm/python/python-embedding').exists() +} + +def configureGraalPyBundleRepository = { File checkoutDir -> + if (isUsableGraalPyBundleRepository(System.getProperty('micronaut.graalpy.bundle.repo'))) { + return + } + String repository = discoverGraalPyBundleRepository(checkoutDir) + if (repository != null) { + System.setProperty('micronaut.graalpy.bundle.repo', repository) + System.setProperty('pyronaut.graalpy.bundle.repo', repository) + System.setProperty('org.gradle.project.micronaut.graalpy.bundle.repo', repository) + System.setProperty('org.gradle.project.pyronaut.graalpy.bundle.repo', repository) + } +} + +if (providers.gradleProperty('micronaut.include.graalpy.extensions').orNull == null && + System.getProperty('org.gradle.project.micronaut.include.graalpy.extensions') == null) { + System.setProperty('org.gradle.project.micronaut.include.graalpy.extensions', 'false') +} + +def includeLocalBuild = { String buildName, String group, File directory, Map modules -> + if (directory?.isDirectory() && new File(directory, '.git').exists()) { + includeBuild(directory) { + name = buildName + dependencySubstitution { + modules.each { artifactId, projectPath -> + substitute(module("${group}:${artifactId}")).using(project(projectPath)) + } + } + } + } +} + +def localMicronautCore = localGitPath('local.git.micronaut-core', 'LOCAL_GIT_MICRONAUT_CORE') +File localMicronautCoreDir = localMicronautCore ? file(localMicronautCore) : null +if (localMicronautCoreDir != null) { + configureGraalPyBundleRepository(localMicronautCoreDir) +} +includeLocalBuild( + 'micronaut-core', + 'io.micronaut', + localMicronautCoreDir, + micronautCoreModules.collectEntries { [(it): ":${it}"] } +) + +def localPyronaut = localGitPath('local.git.pyronaut', 'LOCAL_GIT_PYRONAUT') +File localPyronautDir = localPyronaut ? file(localPyronaut) : null +if (localPyronautDir != null) { + configureGraalPyBundleRepository(localPyronautDir) +} +includeLocalBuild('pyronaut', 'io.micronaut.pyronaut', localPyronautDir, pyronautModules) + +def localMicronautStarter = localGitPath('local.git.micronaut-starter', 'LOCAL_GIT_MICRONAUT_STARTER') +File localMicronautStarterDir = localMicronautStarter ? file(localMicronautStarter) : null +includeLocalBuild('micronaut-starter', 'io.micronaut.starter', localMicronautStarterDir, micronautStarterModules) diff --git a/settings.gradle b/settings.gradle index 1cb3bcef89d..fdf2928e085 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,3 +11,5 @@ plugins { rootProject.name = "micronaut-guides" include 'cli' + +apply from: file('gradle/local-composite-builds.gradle') diff --git a/src/docs/common/snippets/common-enable-distributed-configuration.adoc b/src/docs/common/snippets/common-enable-distributed-configuration.adoc index 1740b7ab4ce..37964755790 100644 --- a/src/docs/common/snippets/common-enable-distributed-configuration.adoc +++ b/src/docs/common/snippets/common-enable-distributed-configuration.adoc @@ -1,5 +1,21 @@ === Enable Distributed Configuration +:only-for-languages:python + +Use Micronaut's native configuration-import integration in `config/application-cloud.toml`: + +resource:application-cloud.toml[tag=distributedconfiguration] + +<1> Select the Micronaut secrets-manager integration. +<2> Identify the remote configuration source. +<3> Set the project identifier or authentication mode as described below. + +Do not create `bootstrap.properties` or `bootstrap.toml`. Keep ordinary settings in `config/application.toml` and activate the `cloud` environment when you want to load remote secrets. Local tests use the `test` environment and do not contact the cloud service. A local database test does not verify access to your remote secrets. + +:only-for-languages: + +:exclude-for-languages:python + Create a `bootstrap.properties` file in the `resources` directory to https://docs.micronaut.io/latest/guide/#bootstrap[enable distributed configuration]. Add the following: @@ -27,4 +43,6 @@ You can disable distributed configuration in a test by annotating a test with: ---- @Property(name = "micronaut.config-client.enabled", value = StringUtils.FALSE) @MicronautTest ----- \ No newline at end of file +---- + +:exclude-for-languages: From e0add739975e2c72280669461d988e55aed49a41 Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Tue, 6 Oct 2026 16:27:27 +0200 Subject: [PATCH 2/9] Use released official Starter for Python guide validation on master Keep the released platform coordinates, remove the obsolete Starter fork checkout, and validate master, python and python-guide-support without native image compilation. --- .github/workflows/python-guides.yml | 12 ++++-------- buildSrc/build.gradle | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/python-guides.yml b/.github/workflows/python-guides.yml index e8710469b46..4de11934ac3 100644 --- a/.github/workflows/python-guides.yml +++ b/.github/workflows/python-guides.yml @@ -2,7 +2,9 @@ name: Validate Python Guides on: push: branches: + - master - python + - python-guide-support paths: - '.github/workflows/python-guides.yml' - 'buildSrc/**' @@ -11,7 +13,9 @@ on: - 'src/docs/common/**' pull_request: branches: + - master - python + - python-guide-support paths: - '.github/workflows/python-guides.yml' - 'buildSrc/**' @@ -23,16 +27,8 @@ jobs: validate: name: Generate Python Guides runs-on: ubuntu-latest - env: - LOCAL_GIT_MICRONAUT_STARTER: ${{ github.workspace }}/micronaut-starter steps: - uses: actions/checkout@v6.0.2 - - uses: actions/checkout@v6.0.2 - with: - repository: n0tl3ss/micronaut-starter - # Pin micronaut-starter#3136 until its fix is merged upstream. - ref: 763920aaa46c06aeebbaba995a66fc3bef643801 - path: micronaut-starter - uses: actions/setup-java@v5.2.0 with: distribution: adopt diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index ecb12e9e102..9512488e84c 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -36,7 +36,7 @@ repositories { } ext { - micronautStarterVersion = "5.2.0-SNAPSHOT" + micronautStarterVersion = "5.2.1" } dependencies { From a7f3be4655d33ea98ff612fb61330c23789c2560 Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Tue, 6 Oct 2026 16:52:23 +0200 Subject: [PATCH 3/9] Keep dedicated Python test commands in CI-generated scripts Remove the obsolete environment-only skip; dedicated Python selection already separates Python and JVM scripts. The existing DocsOnlyGuideTest reproduces the bug with CI=true and the complete 149-test suite passes after the fix. Fixes #1916 --- .../main/groovy/io/micronaut/guides/TestScriptGenerator.groovy | 3 --- 1 file changed, 3 deletions(-) diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/TestScriptGenerator.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/TestScriptGenerator.groovy index fac89293093..2f0c6f46076 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/TestScriptGenerator.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/TestScriptGenerator.groovy @@ -184,9 +184,6 @@ kill_kotlin_daemon () { } String folder = GuideProjectGenerator.folderName(metadata.slug(), guidesOption) BuildTool buildTool = guidesOption.getBuildTool() - if (buildTool == PYRONAUT && System.getenv('CI') != null) { - continue - } if (metadata.apps().any { it.name() == DEFAULT_APP_NAME } ) { if (GuideUtils.shouldSkip(metadata,buildTool, guidesOption.getLanguage())) { continue From d05c16c66544abce193afe62f8a9eaaa9697af48 Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Tue, 6 Oct 2026 16:59:33 +0200 Subject: [PATCH 4/9] Make general guide CI explicitly JVM-only at runtime Keep default guide task selection unchanged. The general workflow excludes only existing dedicated Python runtime tasks, while retaining JVM/native tests and Python generation, docs, ZIPs and scripts. A separately configured Python runtime workflow can use the original task selection. Matrix regression: 12 tests RED before the option, 12 GREEN after it; default selection, implicit/explicit apps, docs-only apps, JVM guides, transitive bases, deduplication and CLI controls preserved. Fixes #1922 --- .github/scripts/guide_ci_tasks.py | 24 ++++-- .github/scripts/test_guide_ci_tasks.py | 109 +++++++++++++++++++++++++ .github/workflows/gradle.yml | 3 + 3 files changed, 130 insertions(+), 6 deletions(-) diff --git a/.github/scripts/guide_ci_tasks.py b/.github/scripts/guide_ci_tasks.py index e3fa5b1b043..fd3712449a4 100644 --- a/.github/scripts/guide_ci_tasks.py +++ b/.github/scripts/guide_ci_tasks.py @@ -18,6 +18,7 @@ class GuideMetadata: slug: str publish: bool base: str | None + python_app: bool = False def kebab_case_to_gradle_name(name: str) -> str: @@ -50,6 +51,7 @@ def load_guides(guides_dir: Path) -> dict[str, GuideMetadata]: slug=slug, publish=metadata.get("publish", True) is not False, base=metadata.get("base"), + python_app="PYTHON" in (metadata.get("languages") or []) and metadata.get("apps") != [], ) return guides @@ -75,12 +77,18 @@ def resolve_impacted_guide_slugs( ) -def tasks_for_changed_files(changed_files: Iterable[str], guides_dir: Path) -> list[str]: +def tasks_for_changed_files( + changed_files: Iterable[str], guides_dir: Path, *, jvm_only: bool = False, +) -> list[str]: guides = load_guides(guides_dir) - return [ - f"{kebab_case_to_gradle_name(slug)}Build" - for slug in resolve_impacted_guide_slugs(changed_files, guides) - ] + tasks = [] + for slug in resolve_impacted_guide_slugs(changed_files, guides): + task_slug = kebab_case_to_gradle_name(slug) + task = f"{task_slug}Build" + if jvm_only and guides[slug].python_app: + task += f" -x {task_slug}RunPythonTestScript" + tasks.append(task) + return tasks def matrix_for_tasks(tasks: Iterable[str]) -> dict[str, list[str]]: @@ -124,6 +132,10 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--guides-dir", help="Guides directory. Defaults to /guides.") parser.add_argument("--base", help="Base commit for git diff.") parser.add_argument("--head", default="HEAD", help="Head commit for git diff.") + parser.add_argument( + "--jvm-only", action="store_true", + help="Exclude only Python runtime test tasks; retain JVM tests and guide generation.", + ) parser.add_argument( "--diff-mode", choices=("range", "merge-base"), @@ -153,7 +165,7 @@ def main() -> int: if args.changed_files is not None else git_changed_files(repo, args.base, args.head, args.diff_mode) ) - tasks = tasks_for_changed_files(changed_files, guides_dir) + tasks = tasks_for_changed_files(changed_files, guides_dir, jvm_only=args.jvm_only) matrix = matrix_for_tasks(tasks) print(f"Changed files: {json.dumps(changed_files)}", file=sys.stderr) diff --git a/.github/scripts/test_guide_ci_tasks.py b/.github/scripts/test_guide_ci_tasks.py index 5d0f07ade53..ce418001a86 100644 --- a/.github/scripts/test_guide_ci_tasks.py +++ b/.github/scripts/test_guide_ci_tasks.py @@ -2,6 +2,8 @@ from __future__ import annotations import json +import subprocess +import sys import tempfile import unittest from pathlib import Path @@ -78,6 +80,107 @@ def test_non_guide_changes_return_empty_task_list(self) -> None: self.assertEqual([], tasks) + def test_default_python_build_remains_unchanged(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata(guides_dir, "python-guide", languages=["JAVA", "PYTHON"]) + + self.assertEqual( + ["pythonGuideBuild"], + guide_ci_tasks.tasks_for_changed_files( + ["guides/python-guide/metadata.json"], guides_dir, + ), + ) + + def test_jvm_only_excludes_python_runner_for_implicit_apps(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata(guides_dir, "python-guide", languages=["JAVA", "PYTHON"]) + + self.assertEqual( + ["pythonGuideBuild -x pythonGuideRunPythonTestScript"], + guide_ci_tasks.tasks_for_changed_files( + ["guides/python-guide/metadata.json"], guides_dir, jvm_only=True, + ), + ) + + def test_jvm_only_excludes_python_runner_for_explicit_apps(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata( + guides_dir, "python-guide", languages=["JAVA", "PYTHON"], + apps=[{"name": "books"}, {"name": "inventory"}], + ) + + self.assertEqual( + ["pythonGuideBuild -x pythonGuideRunPythonTestScript"], + guide_ci_tasks.tasks_for_changed_files( + ["guides/python-guide/python/tests/test_books.py"], guides_dir, jvm_only=True, + ), + ) + + def test_jvm_only_docs_only_guide_has_no_absent_runner_exclusion(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata(guides_dir, "docs-guide", languages=["JAVA", "PYTHON"], apps=[]) + + self.assertEqual( + ["docsGuideBuild"], + guide_ci_tasks.tasks_for_changed_files( + ["guides/docs-guide/docs-guide.adoc"], guides_dir, jvm_only=True, + ), + ) + + def test_jvm_only_retains_jvm_guide_build(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata(guides_dir, "jvm-guide", languages=["JAVA", "GROOVY", "KOTLIN"]) + + self.assertEqual( + ["jvmGuideBuild"], + guide_ci_tasks.tasks_for_changed_files( + ["guides/jvm-guide/metadata.json"], guides_dir, jvm_only=True, + ), + ) + + def test_jvm_only_preserves_transitive_base_impacts_and_deduplication(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata(guides_dir, "root-base", publish=False) + write_metadata(guides_dir, "middle-base", publish=False, base="root-base") + write_metadata( + guides_dir, "python-guide", base="middle-base", languages=["JAVA", "PYTHON"], + ) + + self.assertEqual( + ["pythonGuideBuild -x pythonGuideRunPythonTestScript"], + guide_ci_tasks.tasks_for_changed_files( + ["guides/root-base/common.adoc", "guides/python-guide/metadata.json"], + guides_dir, jvm_only=True, + ), + ) + + def test_cli_jvm_only_flag(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + repo = Path(temp_dir) + guides_dir = repo / "guides" + guides_dir.mkdir() + write_metadata(guides_dir, "python-guide", languages=["JAVA", "PYTHON"]) + + for flags, task in ( + ([], "pythonGuideBuild"), + (["--jvm-only"], "pythonGuideBuild -x pythonGuideRunPythonTestScript"), + ): + with self.subTest(flags=flags): + result = subprocess.run( + [sys.executable, str(Path(guide_ci_tasks.__file__).resolve()), + "--repo", str(repo), "--changed-file", "guides/python-guide/metadata.json", *flags], + capture_output=True, text=True, + ) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual({"group_test_tasks": [task]}, json.loads(result.stdout)) + def write_metadata( guides_dir: Path, @@ -85,6 +188,8 @@ def write_metadata( *, publish: bool = True, base: str | None = None, + languages: list[str] | None = None, + apps: list[dict[str, str]] | None = None, ) -> None: guide_dir = guides_dir / slug guide_dir.mkdir() @@ -93,6 +198,10 @@ def write_metadata( metadata["publish"] = publish if base is not None: metadata["base"] = base + if languages is not None: + metadata["languages"] = languages + if apps is not None: + metadata["apps"] = apps (guide_dir / "metadata.json").write_text(json.dumps(metadata), encoding="utf-8") diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index cb51dc25724..5fc3083a61a 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -17,6 +17,8 @@ jobs: - uses: actions/checkout@v6.0.2 with: fetch-depth: 0 + - name: Validate guide matrix selector + run: python3 -m unittest discover -s .github/scripts -p 'test_*.py' - name: Generate Guide Test Matrix id: test_matrix_step env: @@ -25,6 +27,7 @@ jobs: DIFF_MODE: ${{ github.event_name == 'pull_request' && 'merge-base' || 'range' }} run: | python3 .github/scripts/guide_ci_tasks.py \ + --jvm-only \ --repo "$GITHUB_WORKSPACE" \ --base "$BASE_SHA" \ --head "$HEAD_SHA" \ From 8b5253b89dc76fd8c27041890df61d2282ba3fae Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Tue, 6 Oct 2026 20:39:00 +0200 Subject: [PATCH 5/9] Restrict Python guide CI triggers to master Run on pushes to master and pull requests targeting master only. Keep the existing path filters and validation steps unchanged. --- .github/workflows/python-guides.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/python-guides.yml b/.github/workflows/python-guides.yml index 4de11934ac3..b2f2dc852fe 100644 --- a/.github/workflows/python-guides.yml +++ b/.github/workflows/python-guides.yml @@ -3,8 +3,6 @@ on: push: branches: - master - - python - - python-guide-support paths: - '.github/workflows/python-guides.yml' - 'buildSrc/**' @@ -14,8 +12,6 @@ on: pull_request: branches: - master - - python - - python-guide-support paths: - '.github/workflows/python-guides.yml' - 'buildSrc/**' From d4778570f28045946aa2ebf2d7730bf44fb37a0a Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Tue, 6 Oct 2026 22:12:54 +0200 Subject: [PATCH 6/9] Fix first-run docs-only generation and Python metadata review Create missing output parents in the shared generator and preserve file-conflict errors. Cover fresh nested output and repeated generation. Remove the inert python schema shorthand, retain canonical language parsing, and assert the actual Python language icon. Refs #1910; addresses review feedback on #1915. --- .../guides/GuideProjectGenerator.groovy | 4 +-- .../main/resources/guide-metadata.schema.json | 4 --- .../micronaut/guides/DocsOnlyGuideTest.java | 18 +++++++++++-- .../micronaut/guides/IndexGeneratorTest.java | 3 ++- .../guides/core/GuideParserTest.java | 27 +++++++++++++++++++ 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy index 05d5387eae4..38d72463042 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy @@ -111,9 +111,7 @@ class GuideProjectGenerator implements AutoCloseable { } void generateOne(Guide metadata, File inputDir, File outputDir, Language languageFilter) { - if (!outputDir.exists()) { - assert outputDir.mkdir() - } + Files.createDirectories(outputDir.toPath()) JdkVersion javaVersion = Utils.parseJdkVersion() if (metadata.minimumJavaVersion() != null) { diff --git a/buildSrc/src/main/resources/guide-metadata.schema.json b/buildSrc/src/main/resources/guide-metadata.schema.json index 9fc035876a5..28b55b51489 100644 --- a/buildSrc/src/main/resources/guide-metadata.schema.json +++ b/buildSrc/src/main/resources/guide-metadata.schema.json @@ -50,10 +50,6 @@ "description": "Set it to true to skip running the tests for the Pyronaut applications for the guide", "type": "boolean" }, - "python": { - "description": "Set it to true to generate a Python/Pyronaut variant for the guide", - "type": "boolean" - }, "asciidoctor": { "description": "The guide asciidoc file. If not specified, the guide slug followed by the .adoc suffix is used", "type": "string" diff --git a/buildSrc/src/test/java/io/micronaut/guides/DocsOnlyGuideTest.java b/buildSrc/src/test/java/io/micronaut/guides/DocsOnlyGuideTest.java index 163145b1a06..408bc1ad283 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/DocsOnlyGuideTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/DocsOnlyGuideTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDate; @@ -49,14 +50,27 @@ void emptyAppsProduceNoProjectOutputDirectories() throws Exception { () -> assertEquals(List.of(), SampleProjectGenerationTask.outputDirectoryNames(guide, null)), () -> assertEquals(List.of(), SampleProjectGenerationTask.outputDirectoryNames(guide, Language.PYTHON)) ); + Path outputDirectory = tempDir.resolve("build/code/docs-only"); + assertFalse(Files.exists(outputDirectory.getParent())); try (GuideProjectGenerator generator = new GuideProjectGenerator()) { - generator.generateOne(guide, tempDir.toFile(), tempDir.resolve("code").toFile()); + generator.generateOne(guide, tempDir.toFile(), outputDirectory.toFile()); + generator.generateOne(guide, tempDir.toFile(), outputDirectory.toFile()); } - try (var paths = Files.list(tempDir.resolve("code"))) { + try (var paths = Files.list(outputDirectory)) { assertEquals(0, paths.count()); } } + @Test + void existingFileCannotBeUsedAsProjectOutputDirectory() throws Exception { + Path outputFile = tempDir.resolve("output"); + Files.writeString(outputFile, "not a directory"); + try (GuideProjectGenerator generator = new GuideProjectGenerator()) { + assertThrows(IOException.class, () -> generator.generateOne(guide(List.of()), tempDir.toFile(), outputFile.toFile())); + } + assertEquals("not a directory", Files.readString(outputFile)); + } + @Test void aggregateScriptsDoNotEnterDocsOnlyProjectDirectories() { Guide guide = guide(List.of()); diff --git a/buildSrc/src/test/java/io/micronaut/guides/IndexGeneratorTest.java b/buildSrc/src/test/java/io/micronaut/guides/IndexGeneratorTest.java index 99fb4c8678e..f101596f2c1 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/IndexGeneratorTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/IndexGeneratorTest.java @@ -70,6 +70,7 @@ void pyronautReadLinkPointsToPublishedGuidePage() throws IOException { IndexGenerator.generateGuidesIndex(template, distDir, List.of(guide), null); String html = Files.readString(distDir.toPath().resolve("python-guide.html")); + assertTrue(html.contains("images/python.png")); assertTrue(html.contains("python-guide-pyronaut-python.html'>Read")); assertFalse(html.contains("python-guide.html'>Read")); assertFalse(html.contains("python-guide-gradle-python.html")); @@ -119,7 +120,7 @@ void jvmOnlyGuideDoesNotRenderPythonColumn() throws IOException { IndexGenerator.generateGuidesIndex(template, distDir, List.of(guide), null); String html = Files.readString(distDir.toPath().resolve("jvm-guide.html")); - assertFalse(html.contains("python.svg")); + assertFalse(html.contains("images/python.png")); assertFalse(html.contains("Pyronaut")); } diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java index 24ae18a3ff7..8c92aa5b54f 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java @@ -1,13 +1,18 @@ package io.micronaut.guides.core; +import io.micronaut.core.io.ResourceLoader; import io.micronaut.starter.application.ApplicationType; import io.micronaut.starter.options.BuildTool; import io.micronaut.starter.options.Language; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; +import org.json.JSONException; +import org.json.JSONObject; import org.junit.jupiter.api.Test; import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.util.Collections; import java.util.List; @@ -20,6 +25,28 @@ public class GuideParserTest { @Inject GuideParser guideParser; + @Test + void schemaAdvertisesSupportedPythonOptionsOnly(ResourceLoader resourceLoader) throws IOException, JSONException { + try (var input = resourceLoader.getResourceAsStream("classpath:guide-metadata.schema.json").orElseThrow()) { + JSONObject properties = new JSONObject(new String(input.readAllBytes(), StandardCharsets.UTF_8)) + .getJSONObject("properties"); + assertFalse(properties.has("python")); + assertTrue(properties.getJSONObject("languages").getJSONObject("items") + .getJSONArray("enum").toString().contains("\"PYTHON\"")); + assertTrue(properties.getJSONObject("apps").getJSONObject("items") + .getJSONObject("properties").has("pythonFeatures")); + } + } + + @Test + void explicitPythonLanguageAddsPyronautBuildTool() { + Guide guide = guideParser.parseGuideMetadata( + new File("src/test/resources/guides-python/creating-your-first-micronaut-app"), + "metadata.json").orElseThrow(); + assertEquals(List.of(Language.PYTHON), guide.languages()); + assertEquals(List.of(BuildTool.GRADLE, BuildTool.MAVEN, BuildTool.PYRONAUT), guide.buildTools()); + } + @Test void testParseGuidesMetadata() { String path = "src/test/resources/guides"; From 1ffbfa72b9b862fc8a97611d701f746e4c3cd53b Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Tue, 6 Oct 2026 23:32:49 +0200 Subject: [PATCH 7/9] Match CI Python runner selection to merged guide applications Handle inherited applications in the same sorted pass as GuideUtils and treat omitted applications as empty. Verify parser language defaults and all four language-filter renderings. Trigger Python validation when the local-composite build configuration changes. --- .github/scripts/guide_ci_tasks.py | 13 ++- .github/scripts/test_guide_ci_tasks.py | 81 ++++++++++++++++++- .github/workflows/python-guides.yml | 2 + .../guides/GuideAsciidocGeneratorTest.java | 18 ++--- .../guides/core/GuideParserTest.java | 30 +++++++ 5 files changed, 127 insertions(+), 17 deletions(-) diff --git a/.github/scripts/guide_ci_tasks.py b/.github/scripts/guide_ci_tasks.py index fd3712449a4..795487f1796 100644 --- a/.github/scripts/guide_ci_tasks.py +++ b/.github/scripts/guide_ci_tasks.py @@ -42,16 +42,21 @@ def guide_slugs_from_paths(paths: Iterable[str]) -> set[str]: def load_guides(guides_dir: Path) -> dict[str, GuideMetadata]: - guides = {} + metadata_by_slug = {} for metadata_path in sorted(guides_dir.glob("*/metadata.json")): with metadata_path.open(encoding="utf-8") as metadata_file: - metadata = json.load(metadata_file) - slug = metadata_path.parent.name + metadata_by_slug[metadata_path.parent.name] = json.load(metadata_file) + has_apps = {slug: bool(metadata.get("apps")) for slug, metadata in metadata_by_slug.items()} + # Match GuideUtils' sorted, in-place merge of base applications. + for slug, metadata in metadata_by_slug.items(): + has_apps[slug] = has_apps[slug] or has_apps.get(metadata.get("base"), False) + guides = {} + for slug, metadata in metadata_by_slug.items(): guides[slug] = GuideMetadata( slug=slug, publish=metadata.get("publish", True) is not False, base=metadata.get("base"), - python_app="PYTHON" in (metadata.get("languages") or []) and metadata.get("apps") != [], + python_app="PYTHON" in (metadata.get("languages") or []) and has_apps[slug], ) return guides diff --git a/.github/scripts/test_guide_ci_tasks.py b/.github/scripts/test_guide_ci_tasks.py index ce418001a86..1ef1a95c173 100644 --- a/.github/scripts/test_guide_ci_tasks.py +++ b/.github/scripts/test_guide_ci_tasks.py @@ -83,7 +83,7 @@ def test_non_guide_changes_return_empty_task_list(self) -> None: def test_default_python_build_remains_unchanged(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: guides_dir = Path(temp_dir) - write_metadata(guides_dir, "python-guide", languages=["JAVA", "PYTHON"]) + write_metadata(guides_dir, "python-guide", languages=["JAVA", "PYTHON"], apps=[{"name": "default"}]) self.assertEqual( ["pythonGuideBuild"], @@ -92,13 +92,13 @@ def test_default_python_build_remains_unchanged(self) -> None: ), ) - def test_jvm_only_excludes_python_runner_for_implicit_apps(self) -> None: + def test_jvm_only_guide_without_apps_has_no_absent_runner_exclusion(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: guides_dir = Path(temp_dir) write_metadata(guides_dir, "python-guide", languages=["JAVA", "PYTHON"]) self.assertEqual( - ["pythonGuideBuild -x pythonGuideRunPythonTestScript"], + ["pythonGuideBuild"], guide_ci_tasks.tasks_for_changed_files( ["guides/python-guide/metadata.json"], guides_dir, jvm_only=True, ), @@ -143,6 +143,78 @@ def test_jvm_only_retains_jvm_guide_build(self) -> None: ), ) + def test_jvm_only_excludes_python_runner_for_inherited_apps(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata(guides_dir, "app-base", publish=False, apps=[{"name": "default"}]) + write_metadata(guides_dir, "python-guide", base="app-base", languages=["PYTHON"], apps=[]) + + self.assertEqual( + ["pythonGuideBuild -x pythonGuideRunPythonTestScript"], + guide_ci_tasks.tasks_for_changed_files( + ["guides/python-guide/metadata.json"], guides_dir, jvm_only=True, + ), + ) + + def test_jvm_only_resolves_sorted_transitive_app_merges(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata(guides_dir, "a-base", publish=False, apps=[{"name": "default"}]) + write_metadata(guides_dir, "b-base", publish=False, base="a-base", apps=[]) + write_metadata(guides_dir, "python-guide", base="b-base", languages=["PYTHON"], apps=[]) + + self.assertEqual( + ["pythonGuideBuild -x pythonGuideRunPythonTestScript"], + guide_ci_tasks.tasks_for_changed_files( + ["guides/a-base/common.adoc"], guides_dir, jvm_only=True, + ), + ) + + def test_jvm_only_does_not_inherit_base_python_language(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata( + guides_dir, "python-base", publish=False, languages=["PYTHON"], + apps=[{"name": "default"}], + ) + for languages in (None, ["JAVA", "GROOVY", "KOTLIN"]): + with self.subTest(languages=languages): + child = "default-jvm" if languages is None else "explicit-jvm" + write_metadata(guides_dir, child, base="python-base", languages=languages, apps=[]) + self.assertEqual( + [f"{guide_ci_tasks.kebab_case_to_gradle_name(child)}Build"], + guide_ci_tasks.tasks_for_changed_files( + [f"guides/{child}/metadata.json"], guides_dir, jvm_only=True, + ), + ) + + def test_jvm_only_empty_base_and_child_have_no_python_runner(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata(guides_dir, "docs-base", publish=False, languages=["PYTHON"], apps=[]) + write_metadata(guides_dir, "docs-guide", base="docs-base", languages=["PYTHON"], apps=[]) + + self.assertEqual( + ["docsGuideBuild"], + guide_ci_tasks.tasks_for_changed_files( + ["guides/docs-base/common.adoc"], guides_dir, jvm_only=True, + ), + ) + + def test_jvm_only_preserves_sorted_merge_order_for_later_bases(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + guides_dir = Path(temp_dir) + write_metadata(guides_dir, "z-base", publish=False, apps=[{"name": "default"}]) + write_metadata(guides_dir, "b-base", publish=False, base="z-base", apps=[]) + write_metadata(guides_dir, "a-guide", base="b-base", languages=["PYTHON"], apps=[]) + + self.assertEqual( + ["aGuideBuild"], + guide_ci_tasks.tasks_for_changed_files( + ["guides/z-base/common.adoc"], guides_dir, jvm_only=True, + ), + ) + def test_jvm_only_preserves_transitive_base_impacts_and_deduplication(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: guides_dir = Path(temp_dir) @@ -150,6 +222,7 @@ def test_jvm_only_preserves_transitive_base_impacts_and_deduplication(self) -> N write_metadata(guides_dir, "middle-base", publish=False, base="root-base") write_metadata( guides_dir, "python-guide", base="middle-base", languages=["JAVA", "PYTHON"], + apps=[{"name": "default"}], ) self.assertEqual( @@ -165,7 +238,7 @@ def test_cli_jvm_only_flag(self) -> None: repo = Path(temp_dir) guides_dir = repo / "guides" guides_dir.mkdir() - write_metadata(guides_dir, "python-guide", languages=["JAVA", "PYTHON"]) + write_metadata(guides_dir, "python-guide", languages=["JAVA", "PYTHON"], apps=[{"name": "default"}]) for flags, task in ( ([], "pythonGuideBuild"), diff --git a/.github/workflows/python-guides.yml b/.github/workflows/python-guides.yml index b2f2dc852fe..025d5667e60 100644 --- a/.github/workflows/python-guides.yml +++ b/.github/workflows/python-guides.yml @@ -6,6 +6,7 @@ on: paths: - '.github/workflows/python-guides.yml' - 'buildSrc/**' + - 'gradle/local-composite-builds.gradle' - 'guides/**' - 'settings.gradle' - 'src/docs/common/**' @@ -15,6 +16,7 @@ on: paths: - '.github/workflows/python-guides.yml' - 'buildSrc/**' + - 'gradle/local-composite-builds.gradle' - 'guides/**' - 'settings.gradle' - 'src/docs/common/**' diff --git a/buildSrc/src/test/java/io/micronaut/guides/GuideAsciidocGeneratorTest.java b/buildSrc/src/test/java/io/micronaut/guides/GuideAsciidocGeneratorTest.java index c35213d72e4..6f9806ee42b 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/GuideAsciidocGeneratorTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/GuideAsciidocGeneratorTest.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -42,20 +43,19 @@ void onlyForLanguagesEndsAtClosingMarker() throws Exception { Guide guide = new Guide( "Guide", "Guide.", List.of("Micronaut"), List.of("Core Basics"), LocalDate.of(2026, 9, 22), - null, null, null, false, false, "guide.adoc", List.of(Language.JAVA, Language.PYTHON), List.of(), + null, null, null, false, false, "guide.adoc", List.of(Language.JAVA, Language.GROOVY, Language.KOTLIN, Language.PYTHON), List.of(), List.of(BuildTool.GRADLE, BuildTool.PYRONAUT), TestFramework.JUNIT, List.of(), "guide", true, null, Map.of(), List.of(new App("default", null, null, null, List.of(), null, null, null, null, null, null, null, true)), false); GuideAsciidocGenerator.generate(guide, input.toFile(), output.toFile(), project.toFile()); - String java = Files.readString(output.resolve("guide-gradle-java.adoc")); - String python = Files.readString(output.resolve("guide-pyronaut-python.adoc")); - assertTrue(java.contains("before")); - assertFalse(java.contains("python-only")); - assertTrue(java.contains("after")); - assertTrue(python.contains("before")); - assertTrue(python.contains("python-only")); - assertTrue(python.contains("after")); + for (Language language : guide.languages()) { + String buildTool = language == Language.PYTHON ? "pyronaut" : "gradle"; + String rendered = Files.readString(output.resolve("guide-" + buildTool + "-" + language.name().toLowerCase() + ".adoc")); + assertTrue(rendered.contains("before")); + assertEquals(language == Language.PYTHON, rendered.contains("python-only")); + assertTrue(rendered.contains("after")); + } } @Test diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java index 8c92aa5b54f..8429496e68c 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/GuideParserTest.java @@ -6,13 +6,17 @@ import io.micronaut.starter.options.Language; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.LocalDate; import java.util.Collections; import java.util.List; @@ -47,6 +51,32 @@ void explicitPythonLanguageAddsPyronautBuildTool() { assertEquals(List.of(BuildTool.GRADLE, BuildTool.MAVEN, BuildTool.PYRONAUT), guide.buildTools()); } + @Test + void childLanguageDefaultsAreAppliedBeforeBaseAppsAreMerged(@TempDir Path guides) throws Exception { + Path base = Files.createDirectory(guides.resolve("base")); + Files.writeString(base.resolve("metadata.json"), """ + {"publish":false,"languages":["PYTHON"],"apps":[{"name":"default"}]} + """); + Path child = Files.createDirectory(guides.resolve("child")); + JSONObject metadata = new JSONObject(Files.readString( + Path.of("src/test/resources/guides-python/creating-your-first-micronaut-app/metadata.json"))); + metadata.remove("languages"); + metadata.put("base", "base"); + metadata.put("apps", new JSONArray()); + for (boolean python : List.of(false, true)) { + if (python) { + metadata.put("languages", new JSONArray(List.of("PYTHON"))); + } + Files.writeString(child.resolve("metadata.json"), metadata.toString()); + Guide parsed = guideParser.parseGuidesMetadata(guides.toFile(), "metadata.json").stream() + .filter(guide -> guide.slug().equals("child")).findFirst().orElseThrow(); + assertEquals(python ? List.of(Language.PYTHON) : List.of(Language.JAVA, Language.GROOVY, Language.KOTLIN), parsed.languages()); + assertEquals(python, parsed.buildTools().contains(BuildTool.PYRONAUT)); + assertEquals(1, parsed.apps().size()); + assertEquals("default", parsed.apps().get(0).name()); + } + } + @Test void testParseGuidesMetadata() { String path = "src/test/resources/guides"; From 12cbce063569948dd8f857b33bc6f176e386efdd Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Wed, 7 Oct 2026 00:03:07 +0200 Subject: [PATCH 8/9] Track merged Python script inputs and normalize mixed module names Fingerprint the merged Guide record value, so base-only application changes invalidate generated scripts without requiring serializer service loading during Gradle input evaluation. Normalize mixed-case identifiers with underscores in both existing filename helpers. Add parser-backed input and filesystem-independent exclusion regressions. Fixes #1923 Fixes #1925 --- .../guides/GuideAsciidocGenerator.groovy | 2 +- .../guides/GuideProjectGenerator.groovy | 2 +- .../guides/tasks/PythonTestScriptTask.groovy | 5 ++ .../guides/GuideProjectGeneratorTest.java | 85 +++++++++++++++++++ .../tasks/PythonTestScriptTaskTest.java | 55 ++++++++++++ 5 files changed, 147 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/GuideAsciidocGenerator.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/GuideAsciidocGenerator.groovy index 2d1f5a3f01d..5ef5baca962 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/GuideAsciidocGenerator.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/GuideAsciidocGenerator.groovy @@ -653,7 +653,7 @@ class GuideAsciidocGenerator { } private static String pythonModuleName(String target) { - if (target.contains('_') || target == target.toLowerCase(Locale.ENGLISH)) { + if (target == target.toLowerCase(Locale.ENGLISH)) { return target } target.replaceAll(/([a-z0-9])([A-Z])/, '$1_$2') diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy index 38d72463042..bb84fb9fc45 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy @@ -229,7 +229,7 @@ class GuideProjectGenerator implements AutoCloseable { } private static String pythonModuleName(String target) { - if (target.contains('_') || target == target.toLowerCase(Locale.ENGLISH)) { + if (target == target.toLowerCase(Locale.ENGLISH)) { return target } target.replaceAll(/([a-z0-9])([A-Z])/, '$1_$2') diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/tasks/PythonTestScriptTask.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/tasks/PythonTestScriptTask.groovy index 6dec74d94c5..27e371c7cdc 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/tasks/PythonTestScriptTask.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/tasks/PythonTestScriptTask.groovy @@ -34,6 +34,11 @@ abstract class PythonTestScriptTask extends DefaultTask { @Internal Guide metadata + @Input + String getMergedMetadata() { + metadata.toString() + } + @OutputFile abstract RegularFileProperty getScriptFile() diff --git a/buildSrc/src/test/java/io/micronaut/guides/GuideProjectGeneratorTest.java b/buildSrc/src/test/java/io/micronaut/guides/GuideProjectGeneratorTest.java index c68161566a2..4ea8119d71e 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/GuideProjectGeneratorTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/GuideProjectGeneratorTest.java @@ -1,5 +1,12 @@ package io.micronaut.guides; +import io.micronaut.guides.core.App; +import io.micronaut.guides.core.Guide; +import io.micronaut.guides.core.GuidesOption; +import io.micronaut.starter.api.TestFramework; +import io.micronaut.starter.application.ApplicationType; +import io.micronaut.starter.options.BuildTool; +import io.micronaut.starter.options.Language; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -7,7 +14,11 @@ import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -17,6 +28,80 @@ class GuideProjectGeneratorTest { @TempDir Path directory; + @Test + void mixedCaseUnderscoreExclusionsRemovePythonSourceAndPackagedAndFlatTests() throws Exception { + Path project = generatePythonExclusionProject("Hello_Controller", "Hello_ControllerTest"); + GuidesOption option = new GuidesOption(BuildTool.PYRONAUT, Language.PYTHON, TestFramework.PYTEST); + Method moduleName = GuideProjectGenerator.class.getDeclaredMethod("pythonModuleName", String.class); + moduleName.setAccessible(true); + Method testModuleName = GuideProjectGenerator.class.getDeclaredMethod("pythonTestModuleName", String.class); + testModuleName.setAccessible(true); + + assertAll( + () -> assertEquals("hello_controller", moduleName.invoke(null, "Hello_Controller")), + () -> assertEquals("test_hello_controller", testModuleName.invoke(null, "Hello_ControllerTest")), + () -> assertEquals("src/example/micronaut/hello_controller.py", + GuideAsciidocGenerator.mainPath("", "Hello_Controller", option)), + () -> assertEquals("tests/example/micronaut/test_hello_controller.py", + GuideAsciidocGenerator.testPath("", "Hello_ControllerTest", option)), + () -> assertFalse(Files.exists(project.resolve("src/example/micronaut/hello_controller.py"))), + () -> assertFalse(Files.exists(project.resolve("tests/example/micronaut/test_hello_controller.py"))), + () -> assertFalse(Files.exists(project.resolve("tests/test_hello_controller.py"))) + ); + } + + @Test + void lowercaseExclusionsPreserveAlreadyCorrectPythonModuleNames() throws Exception { + Path project = generatePythonExclusionProject("hello_controller", "test_hello_controller"); + GuidesOption option = new GuidesOption(BuildTool.PYRONAUT, Language.PYTHON, TestFramework.PYTEST); + Method moduleName = GuideProjectGenerator.class.getDeclaredMethod("pythonModuleName", String.class); + moduleName.setAccessible(true); + Method testModuleName = GuideProjectGenerator.class.getDeclaredMethod("pythonTestModuleName", String.class); + testModuleName.setAccessible(true); + + assertAll( + () -> assertEquals("hello_controller", moduleName.invoke(null, "hello_controller")), + () -> assertEquals("test_hello_controller", testModuleName.invoke(null, "test_hello_controller")), + () -> assertEquals("src/example/micronaut/hello_controller.py", + GuideAsciidocGenerator.mainPath("", "hello_controller", option)), + () -> assertEquals("tests/example/micronaut/test_hello_controller.py", + GuideAsciidocGenerator.testPath("", "test_hello_controller", option)), + () -> assertFalse(Files.exists(project.resolve("src/example/micronaut/hello_controller.py"))), + () -> assertFalse(Files.exists(project.resolve("tests/example/micronaut/test_hello_controller.py"))), + () -> assertFalse(Files.exists(project.resolve("tests/test_hello_controller.py"))) + ); + } + + private Path generatePythonExclusionProject(String sourceIdentifier, String testIdentifier) throws Exception { + Path input = Files.createDirectory(directory.resolve("guide")); + for (String path : List.of("src/example/micronaut/hello_controller.py", "src/example/micronaut/retained.py", + "tests/example/micronaut/test_hello_controller.py", "tests/example/micronaut/test_retained.py", + "tests/test_hello_controller.py", "tests/test_retained.py")) { + Path file = input.resolve("python/" + path); + Files.createDirectories(file.getParent()); + Files.writeString(file, "pass\n"); + } + App app = new App("default", "example.micronaut", ApplicationType.DEFAULT, "Micronaut", + List.of(), List.of(), List.of(), List.of(), List.of(), + TestFramework.PYTEST, List.of(testIdentifier), List.of(sourceIdentifier), false); + Guide guide = new Guide("Python exclusions", "Exclude Python modules.", List.of("Micronaut"), + List.of("Getting Started"), LocalDate.of(2026, 10, 6), null, null, null, false, false, + "python-exclusions.adoc", List.of(Language.PYTHON), List.of(), List.of(BuildTool.PYRONAUT), + TestFramework.PYTEST, List.of(), "python-exclusions", true, null, Map.of(), List.of(app), false); + Path output = directory.resolve("output"); + try (GuideProjectGenerator generator = new GuideProjectGenerator()) { + generator.generateOne(guide, input.toFile(), output.toFile(), Language.PYTHON); + } + Path project = output.resolve("python-exclusions-pyronaut-python"); + assertAll( + () -> assertTrue(Files.exists(project.resolve("pyproject.toml"))), + () -> assertTrue(Files.exists(project.resolve("src/example/micronaut/retained.py"))), + () -> assertTrue(Files.exists(project.resolve("tests/example/micronaut/test_retained.py"))), + () -> assertTrue(Files.exists(project.resolve("tests/test_retained.py"))) + ); + return project; + } + @Test void pythonResourceTransferDoesNotInheritJvmBootstrapConfiguration() throws Exception { File input = new File("src/test/resources/file-transfer/python-application-config"); diff --git a/buildSrc/src/test/java/io/micronaut/guides/tasks/PythonTestScriptTaskTest.java b/buildSrc/src/test/java/io/micronaut/guides/tasks/PythonTestScriptTaskTest.java index 3e5891e011a..71346315a7b 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/tasks/PythonTestScriptTaskTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/tasks/PythonTestScriptTaskTest.java @@ -1,19 +1,74 @@ package io.micronaut.guides.tasks; +import io.micronaut.guides.core.DefaultGuideParser; +import io.micronaut.guides.core.DefaultJsonSchemaProvider; +import io.micronaut.guides.core.Guide; +import io.micronaut.json.JsonMapper; +import org.gradle.api.Project; import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.PathSensitive; import org.gradle.api.tasks.PathSensitivity; +import org.gradle.testfixtures.ProjectBuilder; +import org.json.JSONArray; +import org.json.JSONObject; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; class PythonTestScriptTaskTest { + @Test + void inheritedAppsChangeTheDeclaredInputsAndGeneratedScript(@TempDir Path directory) throws Exception { + Path guides = Files.createDirectory(directory.resolve("guides")); + Path base = Files.createDirectory(guides.resolve("a-base")); + Path child = Files.createDirectory(guides.resolve("child")); + Path metadataFile = child.resolve("metadata.json"); + JSONObject childMetadata = new JSONObject(Files.readString( + Path.of("src/test/resources/guides-python/creating-your-first-micronaut-app/metadata.json"))); + childMetadata.put("base", "a-base"); + childMetadata.put("apps", new JSONArray()); + Files.writeString(metadataFile, childMetadata.toString()); + String unchangedChild = Files.readString(metadataFile); + Path functions = Files.writeString(directory.resolve("functions.sh"), "# unchanged helper\n"); + Project project = ProjectBuilder.builder().withProjectDir(directory.toFile()).build(); + PythonTestScriptTask task = project.getTasks().create("pythonTestScript", PythonTestScriptTask.class); + task.getGuideSlug().set("child"); + task.getMetadataFile().set(metadataFile.toFile()); + task.getPyronautTestFunctionsFile().set(functions.toFile()); + task.getScriptFile().set(directory.resolve("python-test.sh").toFile()); + DefaultGuideParser parser = new DefaultGuideParser(new DefaultJsonSchemaProvider(), JsonMapper.createDefault()); + Map originalInputs = null; + String originalScript = null; + for (String appName : new String[]{"first", "second"}) { + Files.writeString(base.resolve("metadata.json"), + "{\"publish\":false,\"apps\":[{\"name\":\"" + appName + "\"}]}"); + Guide merged = parser.parseGuidesMetadata(guides.toFile(), "metadata.json").stream() + .filter(guide -> guide.slug().equals("child")).findFirst().orElseThrow(); + task.setMetadata(merged); + Map inputs = Map.copyOf(task.getInputs().getProperties()); + task.perform(); + String script = Files.readString(task.getScriptFile().get().getAsFile().toPath()); + assertTrue(script.contains("cd " + appName + "\n")); + if (originalInputs != null) { + assertNotEquals(originalScript, script); + assertNotEquals(originalInputs, inputs, "Changing inherited applications must invalidate the script inputs"); + } + originalInputs = inputs; + originalScript = script; + } + assertEquals(unchangedChild, Files.readString(metadataFile)); + } + @Test void pyronautTestFunctionsResourceIsAnInput() { Method property = Arrays.stream(PythonTestScriptTask.class.getMethods()) From f9cd35baab9922d628d76b56703469523ce69d3e Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Wed, 7 Oct 2026 09:51:42 +0200 Subject: [PATCH 9/9] Narrow Python prerequisites and use the installed released CLI Defer H2 and cloud-family additions; remove mandatory local composite SDK staging and unrelated maintenance. Propagate setup failures and reject unsupported Python POM macros. Fixes #1926 Fixes #1927 Fixes #1928 --- .../scripts/test_pyronaut_test_functions.py | 73 +++++++ .github/workflows/python-guides.yml | 2 - README.md | 19 +- buildSrc/build.gradle | 2 +- buildSrc/settings.gradle | 10 - .../io/micronaut/guides/GuidesPlugin.groovy | 111 +--------- .../core/DefaultGuideProjectGenerator.java | 4 +- .../guides/core/DependencyLines.java | 3 + .../micronaut/guides/feature/H2Runtime.java | 11 - .../guides/feature/SpringSecurityCrypto.java | 27 --- buildSrc/src/main/resources/pom.xml | 4 - .../main/resources/pyronaut-test-functions.sh | 192 +----------------- .../io/micronaut/guides/GuidesPluginTest.java | 32 ++- .../io/micronaut/guides/core/AppTest.java | 2 +- .../core/DependencyMacroSubstitutionTest.java | 29 +++ .../core/GuideProjectGeneratorTest.java | 26 --- .../io/micronaut/guides/core/GuideTest.java | 4 +- .../guides/core/TestScriptGeneratorTest.java | 37 +--- gradle/local-composite-builds.gradle | 151 -------------- settings.gradle | 2 - ...mmon-enable-distributed-configuration.adoc | 20 +- 21 files changed, 162 insertions(+), 599 deletions(-) create mode 100644 .github/scripts/test_pyronaut_test_functions.py delete mode 100644 buildSrc/settings.gradle delete mode 100644 buildSrc/src/main/java/io/micronaut/guides/feature/H2Runtime.java delete mode 100644 buildSrc/src/main/java/io/micronaut/guides/feature/SpringSecurityCrypto.java delete mode 100644 gradle/local-composite-builds.gradle diff --git a/.github/scripts/test_pyronaut_test_functions.py b/.github/scripts/test_pyronaut_test_functions.py new file mode 100644 index 00000000000..1420f5ddf29 --- /dev/null +++ b/.github/scripts/test_pyronaut_test_functions.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + + +HELPER = Path(__file__).resolve().parents[2] / "buildSrc/src/main/resources/pyronaut-test-functions.sh" +STEPS = ("setup", "install", "validate-config", "process", "test") +HARNESS = r''' +set -e +source "$1" +set_pyronaut_test_resources() { + printf 'setup\n' >> "$COMMAND_LOG" + if [[ "$FAILED_STEP" == "setup" ]]; then return 42; fi +} +pyronaut() { + printf '%s\n' "$1" >> "$COMMAND_LOG" + if [[ "$FAILED_STEP" == "$1" ]]; then return 42; fi +} +run_pyronaut_install() { pyronaut install; } +run_pyronaut_validate_config() { pyronaut validate-config; } +run_pyronaut_process() { pyronaut process; } +run_pyronaut_test() { pyronaut test; } +status=0 +run_pyronaut_tests || status=$? +exit "$status" +''' + + +class PyronautTestFunctionsTest(unittest.TestCase): + def run_helper(self, failed_step: str): + with tempfile.TemporaryDirectory() as directory: + command_log = Path(directory) / "commands.log" + completed = subprocess.run( + ["/bin/bash", "-c", HARNESS, "helper-test", str(HELPER)], + cwd=directory, + env={"PATH": os.defpath, "COMMAND_LOG": str(command_log), "FAILED_STEP": failed_step}, + text=True, capture_output=True, timeout=5, + ) + commands = command_log.read_text().splitlines() if command_log.exists() else [] + return completed, commands + + def check_failure(self, step: str): + completed, commands = self.run_helper(step) + self.assertEqual(42, completed.returncode, f"commands={commands}, stderr={completed.stderr}") + self.assertEqual(list(STEPS[:STEPS.index(step) + 1]), commands) + + def test_setup_failure_stops_install_and_tests(self): + self.check_failure("setup") + + def test_install_failure_stops_validation_and_tests(self): + self.check_failure("install") + + def test_validation_failure_stops_processing_and_tests(self): + self.check_failure("validate-config") + + def test_processing_failure_stops_tests(self): + self.check_failure("process") + + def test_test_failure_is_propagated(self): + self.check_failure("test") + + def test_success_executes_every_step_once(self): + completed, commands = self.run_helper("") + self.assertEqual(0, completed.returncode, completed.stderr) + self.assertEqual(list(STEPS), commands) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/python-guides.yml b/.github/workflows/python-guides.yml index 025d5667e60..b2f2dc852fe 100644 --- a/.github/workflows/python-guides.yml +++ b/.github/workflows/python-guides.yml @@ -6,7 +6,6 @@ on: paths: - '.github/workflows/python-guides.yml' - 'buildSrc/**' - - 'gradle/local-composite-builds.gradle' - 'guides/**' - 'settings.gradle' - 'src/docs/common/**' @@ -16,7 +15,6 @@ on: paths: - '.github/workflows/python-guides.yml' - 'buildSrc/**' - - 'gradle/local-composite-builds.gradle' - 'guides/**' - 'settings.gradle' - 'src/docs/common/**' diff --git a/README.md b/README.md index 47611fd67f2..e2a7a7eda06 100644 --- a/README.md +++ b/README.md @@ -18,21 +18,10 @@ To build a single guide, run the dynamic task created by `GuidesPlugin`; convert ./gradlew micronautHttpClientBuild ``` -### Use local composite builds - -Local Micronaut Core, Pyronaut, and Starter checkouts can be included with -either the matching Gradle property or environment variable: - -| Checkout | Gradle property | Environment variable | -| --- | --- | --- | -| Micronaut Core | `local.git.micronaut-core` | `LOCAL_GIT_MICRONAUT_CORE` | -| Pyronaut | `local.git.pyronaut` | `LOCAL_GIT_PYRONAUT` | -| Micronaut Starter | `local.git.micronaut-starter` | `LOCAL_GIT_MICRONAUT_STARTER` | - -For example: `./gradlew -Plocal.git.pyronaut=/path/to/pyronaut help`. -Gradle properties take precedence when both forms are set. Pyronaut's GraalPy -bundle repository is discovered from a local Core or Pyronaut checkout when -available; set `micronaut.graalpy.bundle.repo` to override it. +Python guide test tasks use the installed `pyronaut` CLI on `PATH`; no local SDK +checkout is required. CI can provision the released tools with +[setup-pyronaut](https://github.com/micronaut-projects/setup-pyronaut). +`generateAllPythonGuideTestScripts` generates projects and scripts without running the SDK. ## Create a new guide diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 9512488e84c..6f683898dc8 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -83,5 +83,5 @@ tasks.named('test', Test) { } tasks.withType(JavaCompile).configureEach { - options.compilerArgs.add("-Amicronaut.jsonschema.baseUri=https://guides.micronaut.io/schemas") // (1) + options.compilerArgs.add("-Amicronaut.jsonschema.baseUri=https://micronaut-projects.github.io/micronaut-guides/schemas") // (1) } diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle deleted file mode 100644 index bf1a7b02461..00000000000 --- a/buildSrc/settings.gradle +++ /dev/null @@ -1,10 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - } -} - -rootProject.name = "micronaut-guides-buildsrc" - -apply from: file('../gradle/local-composite-builds.gradle') diff --git a/buildSrc/src/main/groovy/io/micronaut/guides/GuidesPlugin.groovy b/buildSrc/src/main/groovy/io/micronaut/guides/GuidesPlugin.groovy index 657d8609227..96a149f8fbf 100644 --- a/buildSrc/src/main/groovy/io/micronaut/guides/GuidesPlugin.groovy +++ b/buildSrc/src/main/groovy/io/micronaut/guides/GuidesPlugin.groovy @@ -19,13 +19,11 @@ import io.micronaut.guides.tasks.PythonTestScriptTask import io.micronaut.json.JsonMapper import io.micronaut.starter.options.Language import org.apache.tools.ant.filters.ReplaceTokens -import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.Transformer import org.gradle.api.file.Directory -import org.gradle.api.initialization.IncludedBuild import org.gradle.api.provider.Provider import org.gradle.api.tasks.Copy import org.gradle.api.tasks.TaskProvider @@ -60,49 +58,12 @@ class GuidesPlugin implements Plugin { private static final String KEY_PYTHON_BUILD = "python-build" private static final String COMMA = "," private static final String TASK_SUFFIX_BUILD = "Build" - private static final String LOCAL_GIT_PYRONAUT_PROPERTY = "local.git.pyronaut" - private static final String LOCAL_GIT_PYRONAUT_ENV = "LOCAL_GIT_PYRONAUT" - private static final String LOCAL_PYRONAUT_CORE_VERSION_PROPERTY = "local.pyronaut.core.version" - private static final String LOCAL_PYRONAUT_CORE_VERSION_ENV = "LOCAL_PYRONAUT_CORE_VERSION" - private static final String LOCAL_PYRONAUT_PLATFORM_VERSION_PROPERTY = "local.pyronaut.platform.version" - private static final String LOCAL_PYRONAUT_PLATFORM_VERSION_ENV = "LOCAL_PYRONAUT_PLATFORM_VERSION" - private static final String DEFAULT_LOCAL_PYRONAUT_CORE_VERSION = "5.2.3" - private static final String DEFAULT_LOCAL_PYRONAUT_PLATFORM_VERSION = "5.1.0" - private static final String PYRONAUT_INCLUDED_BUILD_NAME = "pyronaut" - private static final String PYRONAUT_FIXTURE_REPOSITORY = "functional-test/build/fixture-repo" - private static final String PYRONAUT_INSTALL_EXECUTABLE = "pyronaut-install/build/install/micronaut-pyronaut-install/bin/pyronaut-install" - private static final String PYRONAUT_VALIDATE_CONFIG_EXECUTABLE = "pyronaut-validate-config/build/install/micronaut-pyronaut-validate-config/bin/pyronaut-validate-config" - private static final String PYRONAUT_PROCESS_EXECUTABLE = "pyronaut-processor/build/install/micronaut-pyronaut-processor/bin/pyronaut-processor" - private static final String PYRONAUT_TEST_EXECUTABLE = "pyronaut-test/build/install/micronaut-pyronaut-test/bin/pyronaut-test" - private static final String PYRONAUT_TEST_RESOURCES_SERVER_EXECUTABLE = "pyronaut-test-resources-server/build/install/micronaut-pyronaut-test-resources-server/bin/pyronaut-test-resources-server" - private static final String PYRONAUT_CLI_PYTHONPATH = "pyronaut/src/main/python" - private static final String PYRONAUT_FIXTURE_LAUNCHER_TASK = ":micronaut-functional-test:installFixtureLaunchers" - private static final List PYRONAUT_FIXTURE_STAGE_TASKS = List.of( - ":micronaut-functional-test:stagePyronautFixtureArtifacts", - ":micronaut-functional-test:stageMicronautPlatformFixtureArtifact", - ":micronaut-functional-test:stageMicronautCoreFixtureArtifacts", - ":micronaut-functional-test:stageMicronautDataFixtureArtifacts", - ":micronaut-functional-test:stageSourcegenFixtureArtifacts", - ":micronaut-functional-test:stageIncludedCoreExternalFixtureArtifacts", - ":micronaut-functional-test:stageMicronautTestFixtureArtifacts" - ) @Override void apply(Project project) { GuideProjectGenerator projectGenerator = new GuideProjectGenerator() Directory guidesDir = project.layout.projectDirectory.dir("guides") Provider codeDir = project.layout.buildDirectory.dir("code") - Provider localPyronautPath = localGitPath(project, LOCAL_GIT_PYRONAUT_PROPERTY, LOCAL_GIT_PYRONAUT_ENV) - Provider localPyronautCoreVersion = configuredValue(project, LOCAL_PYRONAUT_CORE_VERSION_PROPERTY, LOCAL_PYRONAUT_CORE_VERSION_ENV, DEFAULT_LOCAL_PYRONAUT_CORE_VERSION) - Provider localPyronautPlatformVersion = configuredValue(project, LOCAL_PYRONAUT_PLATFORM_VERSION_PROPERTY, LOCAL_PYRONAUT_PLATFORM_VERSION_ENV, DEFAULT_LOCAL_PYRONAUT_PLATFORM_VERSION) - Provider localPyronautRepository = localPyronautPath.map(path -> new File(path, PYRONAUT_FIXTURE_REPOSITORY).absolutePath) - Provider localPyronautInstallExecutable = localPyronautPath.map(path -> new File(path, PYRONAUT_INSTALL_EXECUTABLE).absolutePath) - Provider localPyronautValidateConfigExecutable = localPyronautPath.map(path -> new File(path, PYRONAUT_VALIDATE_CONFIG_EXECUTABLE).absolutePath) - Provider localPyronautProcessExecutable = localPyronautPath.map(path -> new File(path, PYRONAUT_PROCESS_EXECUTABLE).absolutePath) - Provider localPyronautTestExecutable = localPyronautPath.map(path -> new File(path, PYRONAUT_TEST_EXECUTABLE).absolutePath) - Provider localPyronautTestResourcesServerExecutable = localPyronautPath.map(path -> new File(path, PYRONAUT_TEST_RESOURCES_SERVER_EXECUTABLE).absolutePath) - Provider localPyronautCliPythonPath = localPyronautPath.map(path -> new File(path, PYRONAUT_CLI_PYTHONPATH).absolutePath) - TaskProvider stageLocalPyronautArtifactsTask = registerStageLocalPyronautArtifactsTask(project, localPyronautRepository) Properties testProps = guidesDir.file("tests.properties").asFile.withInputStream { inputStream -> new Properties().tap { load(inputStream) @@ -176,17 +137,7 @@ class GuidesPlugin implements Plugin { project, taskSlug, metadata, - pythonScript, - stageLocalPyronautArtifactsTask, - localPyronautRepository, - localPyronautCoreVersion, - localPyronautPlatformVersion, - localPyronautInstallExecutable, - localPyronautValidateConfigExecutable, - localPyronautProcessExecutable, - localPyronautTestExecutable, - localPyronautTestResourcesServerExecutable, - localPyronautCliPythonPath + pythonScript ) pythonBuildTask = registerGuideBuildForLanguage(project, taskSlug, language.toString().capitalize(), metadata, languageDocTask, languageZipTask, pythonScript, pythonRunner) pythonTestRunnerTask = pythonRunner as TaskProvider @@ -319,41 +270,6 @@ class GuidesPlugin implements Plugin { } } - private static Provider localGitPath(Project project, - String propertyName, - String environmentName) { - project.providers.gradleProperty(propertyName) - .orElse(project.providers.environmentVariable(environmentName)) - } - - private static Provider configuredValue(Project project, - String propertyName, - String environmentName, - String defaultValue) { - project.providers.gradleProperty(propertyName) - .orElse(project.providers.environmentVariable(environmentName)) - .orElse(defaultValue) - } - - private static TaskProvider registerStageLocalPyronautArtifactsTask(Project project, - Provider localPyronautRepository) { - IncludedBuild includedBuild = project.gradle.includedBuilds.find { IncludedBuild build -> - build.name == PYRONAUT_INCLUDED_BUILD_NAME - } - project.tasks.register("stageLocalPyronautArtifacts") { Task it -> - it.group = "build setup" - it.description = "Stages Pyronaut artifacts from the local included Pyronaut checkout into its fixture repository." - it.outputs.dir(localPyronautRepository.map(path -> new File(path))) - if (includedBuild != null) { - it.dependsOn(PYRONAUT_FIXTURE_STAGE_TASKS.collect { String taskPath -> includedBuild.task(taskPath) }) - it.dependsOn(includedBuild.task(PYRONAUT_FIXTURE_LAUNCHER_TASK)) - } else { - it.doFirst { - throw new GradleException("Python guide tests require an included '${PYRONAUT_INCLUDED_BUILD_NAME}' build. Configure ${LOCAL_GIT_PYRONAUT_PROPERTY} or ${LOCAL_GIT_PYRONAUT_ENV}.") - } - } - } - } /** * https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore @@ -525,19 +441,9 @@ class GuidesPlugin implements Plugin { private static TaskProvider registerPythonTestScriptRunnerTask(Project project, String taskSlug, Guide metadata, - TaskProvider pythonTestScriptTask, - TaskProvider stageLocalPyronautArtifactsTask, - Provider localPyronautRepository, - Provider localPyronautCoreVersion, - Provider localPyronautPlatformVersion, - Provider localPyronautInstallExecutable, - Provider localPyronautValidateConfigExecutable, - Provider localPyronautProcessExecutable, - Provider localPyronautTestExecutable, - Provider localPyronautTestResourcesServerExecutable, - Provider localPyronautCliPythonPath) { + TaskProvider pythonTestScriptTask) { project.tasks.register("${taskSlug}RunPythonTestScript", TestScriptRunnerTask) { TestScriptRunnerTask it -> - it.onlyIf { !Utils.skipBecauseOfJavaVersion(metadata) } + it.onlyIf { !Utils.skipBecauseOfJavaVersion(metadata) && !Boolean.TRUE.equals(metadata.skipPyronautTests()) } Provider codeDirectory = project.layout.buildDirectory.dir("code/${metadata.slug()}") @@ -545,20 +451,9 @@ class GuidesPlugin implements Plugin { it.description = "Run the Python tests for the Pyronaut project generated by ${metadata.slug()}" it.environment.set(metadata.env()) - it.environment.put("PYRONAUT_LOCAL_REPOSITORY", localPyronautRepository) - it.environment.put("PYRONAUT_LOCAL_CORE_VERSION", localPyronautCoreVersion) - it.environment.put("PYRONAUT_LOCAL_PLATFORM_VERSION", localPyronautPlatformVersion) - it.environment.put("PYRONAUT_INSTALL_EXECUTABLE", localPyronautInstallExecutable) - it.environment.put("PYRONAUT_VALIDATE_CONFIG_EXECUTABLE", localPyronautValidateConfigExecutable) - it.environment.put("PYRONAUT_PROCESS_EXECUTABLE", localPyronautProcessExecutable) - it.environment.put("PYRONAUT_PROCESSOR_EXECUTABLE", localPyronautProcessExecutable) - it.environment.put("PYRONAUT_TEST_EXECUTABLE", localPyronautTestExecutable) - it.environment.put("PYRONAUT_TEST_RESOURCES_SERVER_EXECUTABLE", localPyronautTestResourcesServerExecutable) - it.environment.put("PYRONAUT_CLI_PYTHONPATH", localPyronautCliPythonPath) it.testScript.set(pythonTestScriptTask.flatMap { t -> t.scriptFile }) it.guideSourceDirectory.set(project.layout.projectDirectory.dir("guides/${metadata.slug()}")) it.outputFile.set(codeDirectory.map(d -> d.file("python-output.log"))) - it.dependsOn(stageLocalPyronautArtifactsTask) } } diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideProjectGenerator.java b/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideProjectGenerator.java index 63b4cbf32cf..45547640165 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideProjectGenerator.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/DefaultGuideProjectGenerator.java @@ -38,7 +38,6 @@ import static io.micronaut.starter.options.BuildTool.GRADLE; import static io.micronaut.starter.options.BuildTool.PYRONAUT; import static io.micronaut.starter.options.JdkVersion.JDK_25; -import static io.micronaut.starter.options.Language.GROOVY; import static io.micronaut.starter.options.Language.PYTHON; @Singleton @@ -92,8 +91,7 @@ public void generate(@NonNull File outputDirectory, @NonNull JdkVersion javaVersion, @NonNull App app) throws IOException { List appFeatures = new ArrayList<>(GuideUtils.getAppFeatures(app, guidesOption.getLanguage())); - if (guidesOption.getLanguage() == GROOVY || - !guidesConfiguration.getJdkVersionsSupportedByGraalvm().contains(javaVersion)) { + if (!guidesConfiguration.getJdkVersionsSupportedByGraalvm().contains(javaVersion)) { appFeatures.remove("graalvm"); } diff --git a/buildSrc/src/main/java/io/micronaut/guides/core/DependencyLines.java b/buildSrc/src/main/java/io/micronaut/guides/core/DependencyLines.java index 6b5faf3647b..10f6d9cad46 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/core/DependencyLines.java +++ b/buildSrc/src/main/java/io/micronaut/guides/core/DependencyLines.java @@ -188,6 +188,9 @@ private static List pyronautAsciidoc(List lines) { } String groupId = attributes.getOrDefault("groupId", "io.micronaut"); + if ("true".equalsIgnoreCase(attributes.get("pom"))) { + throw new IllegalArgumentException("Python dependency macros do not support pom=true; configure dependency management explicitly in pyproject.toml."); + } String version = attributes.get("version"); String coordinate = groupId + ':' + artifactId + (version != null ? ':' + version : ""); String callout = extractCallout(attributes); diff --git a/buildSrc/src/main/java/io/micronaut/guides/feature/H2Runtime.java b/buildSrc/src/main/java/io/micronaut/guides/feature/H2Runtime.java deleted file mode 100644 index be704798909..00000000000 --- a/buildSrc/src/main/java/io/micronaut/guides/feature/H2Runtime.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.micronaut.guides.feature; - -import io.micronaut.starter.build.dependencies.Scope; -import jakarta.inject.Singleton; - -@Singleton -public class H2Runtime extends AbstractFeature { - protected H2Runtime() { - super("h2-runtime", "h2", Scope.RUNTIME); - } -} diff --git a/buildSrc/src/main/java/io/micronaut/guides/feature/SpringSecurityCrypto.java b/buildSrc/src/main/java/io/micronaut/guides/feature/SpringSecurityCrypto.java deleted file mode 100644 index 7c5900ef406..00000000000 --- a/buildSrc/src/main/java/io/micronaut/guides/feature/SpringSecurityCrypto.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.micronaut.guides.feature; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.starter.application.generator.GeneratorContext; -import io.micronaut.starter.build.dependencies.Dependency; -import io.micronaut.starter.build.dependencies.Scope; -import jakarta.inject.Singleton; - -@Requires(missingClasses = "io.micronaut.starter.feature.security.SpringSecurityCrypto") -@Singleton -public class SpringSecurityCrypto extends AbstractFeature { - - public SpringSecurityCrypto() { - super("spring-security-crypto"); - } - - @Override - public void apply(GeneratorContext generatorContext) { - super.apply(generatorContext); - - generatorContext.addDependency(Dependency.builder() - .groupId("org.slf4j") - .artifactId("jcl-over-slf4j") - .scope(Scope.RUNTIME) - .build()); - } -} diff --git a/buildSrc/src/main/resources/pom.xml b/buildSrc/src/main/resources/pom.xml index 1595ddda82f..c69fd8a95d3 100644 --- a/buildSrc/src/main/resources/pom.xml +++ b/buildSrc/src/main/resources/pom.xml @@ -12,10 +12,6 @@ - - com.h2database - h2 - org.skyscreamer jsonassert diff --git a/buildSrc/src/main/resources/pyronaut-test-functions.sh b/buildSrc/src/main/resources/pyronaut-test-functions.sh index 1d937912192..9e0c7c53e7e 100644 --- a/buildSrc/src/main/resources/pyronaut-test-functions.sh +++ b/buildSrc/src/main/resources/pyronaut-test-functions.sh @@ -8,76 +8,6 @@ pyronaut_repo_python () { fi } -set_pyronaut_local_repositories () { - if [ -z "${PYRONAUT_LOCAL_REPOSITORY:-}" ]; then - return 0 - fi - - "$(pyronaut_repo_python)" -c 'exec(__import__("sys").stdin.read())' "$PYRONAUT_LOCAL_REPOSITORY" <<'PY' -from pathlib import Path -import json -import re -import sys - -repository = sys.argv[1] -pyproject = Path("pyproject.toml") -text = pyproject.read_text(encoding="utf-8") -section = "[tool.pyronaut]" -section_index = text.find(section) -if section_index < 0: - raise SystemExit("Missing [tool.pyronaut] in pyproject.toml") - -next_section_index = text.find("\n[", section_index + len(section)) -if next_section_index < 0: - next_section_index = len(text) -section_text = text[section_index:next_section_index] -repositories = f'repositories = [{json.dumps(repository)}, "mavenCentral", "https://central.sonatype.com/repository/maven-snapshots/"]' -section_text, count = re.subn(r'(?ms)^repositories\s*=\s*\[[^\]]*\]', repositories, section_text, count=1) -if count == 0: - section_text = section_text.replace(section + "\n", section + "\n" + repositories + "\n", 1) -text = text[:section_index] + section_text + text[next_section_index:] -pyproject.write_text(text, encoding="utf-8") -PY -} - -set_pyronaut_local_versions () { - if [ -z "${PYRONAUT_LOCAL_CORE_VERSION:-}" ] && [ -z "${PYRONAUT_LOCAL_PLATFORM_VERSION:-}" ]; then - return 0 - fi - - "$(pyronaut_repo_python)" -c 'exec(__import__("sys").stdin.read())' "${PYRONAUT_LOCAL_CORE_VERSION:-}" "${PYRONAUT_LOCAL_PLATFORM_VERSION:-}" <<'PY' -from pathlib import Path -import re -import sys - -core_version = sys.argv[1] -platform_version = sys.argv[2] -pyproject = Path("pyproject.toml") -text = pyproject.read_text(encoding="utf-8") - -def set_section_version(source, section, version): - if not version: - return source - section_index = source.find(section) - if section_index < 0: - raise SystemExit(f"Missing {section} in pyproject.toml") - - next_section_index = source.find("\n[", section_index + len(section)) - if next_section_index < 0: - next_section_index = len(source) - section_text = source[section_index:next_section_index] - replacement = f'version = "{version}"' - section_text, count = re.subn(r'(?m)^version\s*=\s*"[^"]*"', replacement, section_text, count=1) - if count == 0: - section_text = section_text.replace(section + "\n", section + "\n" + replacement + "\n", 1) - return source[:section_index] + section_text + source[next_section_index:] - -text = set_section_version(text, "[tool.pyronaut.core]", core_version) -text = set_section_version(text, "[tool.pyronaut.platform]", platform_version) -pyproject.write_text(text, encoding="utf-8") -PY -} - set_pyronaut_test_resources () { "$(pyronaut_repo_python)" -c 'exec(__import__("sys").stdin.read())' <<'PY' from pathlib import Path @@ -102,122 +32,10 @@ pyproject.write_text(text, encoding="utf-8") PY } -set_pyronaut_native_toolchain () { - if [ "${PYRONAUT_USE_NATIVE_TOOLCHAIN:-}" != "true" ]; then - return 0 - fi - - "$(pyronaut_repo_python)" -c 'exec(__import__("sys").stdin.read())' <<'PY' -from pathlib import Path - -pyproject = Path("pyproject.toml") -text = pyproject.read_text(encoding="utf-8") -section = "[tool.pyronaut.toolchain]" -section_index = text.find(section) -if section_index < 0: - separator = "" if not text or text.endswith("\n") else "\n" - text += f'{separator}\n{section}\ntype = "native"\n' -else: - next_section_index = text.find("\n[", section_index + len(section)) - if next_section_index < 0: - next_section_index = len(text) - section_text = text[section_index:next_section_index] - lines = section_text.splitlines(keepends=True) - for index, line in enumerate(lines): - if line.lstrip().startswith('type') and '=' in line: - line_ending = '\n' if line.endswith('\n') else '' - lines[index] = 'type = "native"' + line_ending - break - else: - lines.insert(1, 'type = "native"\n') - section_text = "".join(lines) - text = text[:section_index] + section_text + text[next_section_index:] -pyproject.write_text(text, encoding="utf-8") -PY -} - -set_pyronaut_project_venv () { - if [ -e .venv ] || [ -L .venv ] || [ -z "${VIRTUAL_ENV:-}" ] || [ ! -x "$VIRTUAL_ENV/bin/python" ]; then - return 0 - fi - ln -s "$VIRTUAL_ENV" .venv -} - -pyronaut_dependency_cache () { - echo "${PYRONAUT_DEPENDENCY_CACHE:-.pyronaut-m2}" -} - -run_pyronaut_install () { - local install_args=() - if [ -n "${PYRONAUT_LOCAL_REPOSITORY:-}" ]; then - install_args+=(--local-repository "$(pyronaut_dependency_cache)") - if [ "${PYRONAUT_REFRESH_DEPENDENCIES:-true}" = "true" ]; then - install_args+=(--refresh) - fi - fi - - if [ -n "${PYRONAUT_INSTALL_EXECUTABLE:-}" ]; then - "$PYRONAUT_INSTALL_EXECUTABLE" "${install_args[@]}" - else - pyronaut install "${install_args[@]}" - fi -} - -run_pyronaut_validate_config () { - if [ -n "${PYRONAUT_VALIDATE_CONFIG_EXECUTABLE:-}" ]; then - "$PYRONAUT_VALIDATE_CONFIG_EXECUTABLE" - else - pyronaut validate-config - fi -} - -run_pyronaut_process () { - if [ -n "${PYRONAUT_PROCESS_EXECUTABLE:-}" ]; then - "$PYRONAUT_PROCESS_EXECUTABLE" - else - pyronaut process - fi -} - -run_pyronaut_cli () { - if [ -n "${PYRONAUT_CLI_PYTHONPATH:-}" ] && [ -d "$PYRONAUT_CLI_PYTHONPATH" ]; then - PYTHONPATH="${PYRONAUT_CLI_PYTHONPATH}${PYTHONPATH:+:$PYTHONPATH}" "$(pyronaut_repo_python)" -m pyronaut_cli_v2 "$@" - else - pyronaut "$@" - fi -} - -run_pyronaut_test () { - local test_args=() - if [ -n "${PYRONAUT_LOCAL_REPOSITORY:-}" ]; then - test_args+=(--local-repository "$(pyronaut_dependency_cache)") - fi - run_pyronaut_cli test "${test_args[@]}" -} - run_pyronaut_tests () { - if [ -z "${PYRONAUT_LOCAL_REPOSITORY:-}" ]; then - set_pyronaut_local_versions - set_pyronaut_test_resources - set_pyronaut_native_toolchain - set_pyronaut_project_venv - run_pyronaut_install && run_pyronaut_validate_config && run_pyronaut_process && run_pyronaut_test - return $? - fi - - local tmp_dir - tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/pyronaut-guide.XXXXXX")" - tar --exclude='./.micronaut' --exclude='./__pyronaut__' --exclude='./.pytest_cache' --exclude='./build' --exclude='./dist' --exclude='./*.egg-info' -cf - . | (cd "$tmp_dir" && tar -xf -) - ( - cd "$tmp_dir" - set_pyronaut_local_repositories - set_pyronaut_local_versions - set_pyronaut_test_resources - set_pyronaut_native_toolchain - set_pyronaut_project_venv - run_pyronaut_install && run_pyronaut_validate_config && run_pyronaut_process && run_pyronaut_test - ) - local result=$? - rm -rf "$tmp_dir" - return $result + set_pyronaut_test_resources && + pyronaut install && + pyronaut validate-config && + pyronaut process && + pyronaut test } diff --git a/buildSrc/src/test/java/io/micronaut/guides/GuidesPluginTest.java b/buildSrc/src/test/java/io/micronaut/guides/GuidesPluginTest.java index a7ecbbad9fc..0ebb73f709c 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/GuidesPluginTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/GuidesPluginTest.java @@ -1,6 +1,7 @@ package io.micronaut.guides; import io.micronaut.guides.tasks.AsciidocGenerationTask; +import io.micronaut.guides.tasks.TestScriptRunnerTask; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.testfixtures.ProjectBuilder; @@ -12,9 +13,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; class GuidesPluginTest { @@ -55,7 +59,31 @@ void orderingIncludesDocumentationTasksRegisteredLater() throws IOException { assertTrue(renderer.getTaskDependencies().getDependencies(renderer).isEmpty()); } + @Test + void releasedPythonRunnerDoesNotRequireLocalCheckouts() throws IOException { + assertPythonRunner(project(false), true); + } + + @Test + void skippedPythonRunnerDoesNotRequireRuntimeSetup() throws IOException { + assertPythonRunner(project(true), false); + } + + private void assertPythonRunner(Project project, boolean enabled) { + TestScriptRunnerTask runner = (TestScriptRunnerTask) project.getTasks().getByName("helloRunPythonTestScript"); + assertAll( + () -> assertFalse(runner.getTaskDependencies().getDependencies(runner).stream() + .anyMatch(task -> task.getName().equals("stageLocalPyronautArtifacts"))), + () -> assertEquals(Map.of("GUIDE_MODE", "fixture"), runner.getEnvironment().get()), + () -> assertEquals(enabled, runner.getOnlyIf().isSatisfiedBy(runner)) + ); + } + private Project project() throws IOException { + return project(false); + } + + private Project project(boolean skipPythonTests) throws IOException { Path guide = Files.createDirectories(directory.resolve("guides/hello")); Files.writeString(directory.resolve("guides/tests.properties"), "numberOfTestGroups=1\n"); Files.writeString(guide.resolve("metadata.json"), """ @@ -65,10 +93,12 @@ private Project project() throws IOException { "authors": ["Micronaut"], "categories": ["Core"], "publicationDate": "2026-10-05", + "skipPyronautTests": %s, + "env": {"GUIDE_MODE": "fixture"}, "languages": ["JAVA", "GROOVY", "KOTLIN", "PYTHON"], "apps": [{"name": "default", "features": []}] } - """); + """.formatted(skipPythonTests)); Project project = ProjectBuilder.builder().withProjectDir(directory.toFile()).build(); project.getExtensions().getExtraProperties().set("metadataConfigName", "metadata.json"); project.getTasks().register("asciidoctor"); diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/AppTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/AppTest.java index 203acd40d52..d080db0041a 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/AppTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/AppTest.java @@ -92,7 +92,7 @@ void buildGeneratesCorrectJsonSchema(ResourceLoader resourceLoader) throws IOExc String expected = """ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://guides.micronaut.io/schemas/app.schema.json", + "$id": "https://micronaut-projects.github.io/micronaut-guides/schemas/app.schema.json", "title": "App", "type": "object", "properties": { diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/DependencyMacroSubstitutionTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/DependencyMacroSubstitutionTest.java index f14bdb0f4bc..546ee4c745f 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/DependencyMacroSubstitutionTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/DependencyMacroSubstitutionTest.java @@ -99,4 +99,33 @@ void pythonDependencyMacrosUsePyprojectToml() { After """, result); } + + @Test + void pythonPomMacrosFailInsteadOfRenderingOrdinaryDependencies() { + GuidesOption python = new GuidesOption(BuildTool.PYRONAUT, Language.PYTHON, TestFramework.PYTEST); + for (String scope : new String[]{"implementation", "annotationProcessor"}) { + for (String value : new String[]{"true", "TRUE"}) { + String macro = "dependency:micronaut-platform[groupId=io.micronaut.platform,scope=" + + scope + ",pom=" + value + "]"; + for (String source : new String[]{macro, ":dependencies:\n" + macro + "\n:dependencies:\n"}) { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> dependencyMacroSubstitution.substitute(source, + GuideTestUtils.guideWithSlug("python-pom"), python)); + assertTrue(error.getMessage().contains("pom=true")); + } + } + } + } + + @Test + void ordinaryPythonDependenciesAndJvmPlatformsRemainSupported() { + String ordinary = "dependency:micronaut-http-client[pom=false]"; + assertTrue(DependencyLines.asciidoc(ordinary, BuildTool.PYRONAUT, Language.PYTHON) + .contains(" \"io.micronaut:micronaut-http-client\",")); + String platform = "dependency:micronaut-platform[groupId=io.micronaut.platform,pom=true]"; + assertTrue(DependencyLines.asciidoc(platform, BuildTool.GRADLE, Language.JAVA) + .contains("implementation platform(\"io.micronaut.platform:micronaut-platform\")")); + assertTrue(DependencyLines.asciidoc(platform, BuildTool.MAVEN, Language.JAVA) + .contains(" pom")); + } } diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectGeneratorTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectGeneratorTest.java index a19693bab5b..09d06759bf7 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectGeneratorTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/GuideProjectGeneratorTest.java @@ -8,17 +8,14 @@ import jakarta.inject.Inject; import org.gradle.api.JavaVersion; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; import java.io.File; import java.nio.file.Paths; -import java.nio.file.Path; import java.time.LocalDate; import java.util.List; import java.util.Map; import static io.micronaut.guides.core.TestUtils.readFile; -import static io.micronaut.starter.options.JdkVersion.JDK_25; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -218,28 +215,5 @@ void testGenerateOracleAtpMavenProjectWithOracleFreeTestResources() { assertTrue(result.contains("micronaut-test-resources-jdbc-oracle-free"), result); } - @Test - void testGenerateOracleAtpPythonProjectWithH2Runtime(@TempDir Path outputDirectory) throws Exception { - App app = new App( - "default", "example.micronaut", ApplicationType.DEFAULT, "Micronaut", - List.of("data-jdbc", "flyway", "oracle-cloud-atp"), - List.of(), List.of(), List.of(), List.of(), null, null, null, null, false, - List.of("h2-runtime") - ); - Guide guide = GuideTestUtils.guideWithSlug("oracle-atp-h2-runtime"); - GuidesOption option = new GuidesOption(BuildTool.PYRONAUT, Language.PYTHON, TestFramework.PYTEST); - - ((DefaultGuideProjectGenerator) guideProjectGenerator).generate( - outputDirectory.toFile(), guide, option, JDK_25, app); - - File dest = outputDirectory.resolve(MacroUtils.getSourceDir(guide.slug(), option)).toFile(); - String result = readFile(new File(dest, "pyproject.toml")); - String runtime = result.substring(result.indexOf("runtime = ["), result.indexOf("build = [")); - assertTrue(runtime.contains("\"com.h2database:h2\""), result); - assertTrue(runtime.contains("\"io.micronaut.oraclecloud:micronaut-oraclecloud-atp\""), result); - String configuration = readFile(new File(dest, "config/application.toml")); - assertTrue(configuration.contains("dialect = 'ORACLE'"), configuration); - } - } diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/GuideTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/GuideTest.java index 4ee8579b058..0007d2b3cd1 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/GuideTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/GuideTest.java @@ -230,7 +230,7 @@ void buildGeneratesCorrectJsonSchema(ResourceLoader resourceLoader) throws IOExc String expected = """ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://guides.micronaut.io/schemas/guide.schema.json", + "$id": "https://micronaut-projects.github.io/micronaut-guides/schemas/guide.schema.json", "title": "Guide", "type": "object", "properties": { @@ -238,7 +238,7 @@ void buildGeneratesCorrectJsonSchema(ResourceLoader resourceLoader) throws IOExc "description": "Applications created for the guide; an empty list denotes a documentation-only guide with no generated application", "type": "array", "items": { - "$ref": "https://guides.micronaut.io/schemas/app.schema.json" + "$ref": "https://micronaut-projects.github.io/micronaut-guides/schemas/app.schema.json" } }, "asciidoctor": { diff --git a/buildSrc/src/test/java/io/micronaut/guides/core/TestScriptGeneratorTest.java b/buildSrc/src/test/java/io/micronaut/guides/core/TestScriptGeneratorTest.java index 4ef0572f49a..a78ab49b7b4 100644 --- a/buildSrc/src/test/java/io/micronaut/guides/core/TestScriptGeneratorTest.java +++ b/buildSrc/src/test/java/io/micronaut/guides/core/TestScriptGeneratorTest.java @@ -150,39 +150,18 @@ void testGeneratePython() { String result = testScriptGenerator.generatePythonTestScript(new ArrayList<>(List.of(guide))); - assertTrue(result.contains("PYRONAUT_LOCAL_REPOSITORY")); - assertTrue(result.contains("PYRONAUT_LOCAL_CORE_VERSION")); - assertTrue(result.contains("PYRONAUT_LOCAL_PLATFORM_VERSION")); - assertTrue(result.contains("PYRONAUT_INSTALL_EXECUTABLE")); - assertTrue(result.contains("PYRONAUT_REFRESH_DEPENDENCIES")); - assertTrue(result.contains("PYRONAUT_DEPENDENCY_CACHE:-.pyronaut-m2")); - assertTrue(result.contains("PYRONAUT_PROCESS_EXECUTABLE")); - assertTrue(result.contains("PYRONAUT_CLI_PYTHONPATH")); - assertTrue(result.contains("set_pyronaut_local_versions")); - int runPyronautTests = result.indexOf("run_pyronaut_tests () {"); - assertTrue(runPyronautTests >= 0); - int directBranch = result.indexOf("if [ -z \"${PYRONAUT_LOCAL_REPOSITORY:-}\" ]; then", runPyronautTests); - int tempBranch = result.indexOf("local tmp_dir", directBranch); - String directBranchScript = result.substring(directBranch, tempBranch); - int localVersionsInDirectBranch = directBranchScript.indexOf("set_pyronaut_local_versions"); - int testResourcesInDirectBranch = directBranchScript.indexOf("set_pyronaut_test_resources"); - assertTrue(localVersionsInDirectBranch >= 0 - && localVersionsInDirectBranch < testResourcesInDirectBranch); - assertTrue(result.contains("[tool.pyronaut.core]")); - assertTrue(result.contains("[tool.pyronaut.platform]")); - assertFalse(result.contains("PYRONAUT_LOCAL_VERSION")); - assertTrue(result.contains("run_pyronaut_install")); - assertTrue(result.contains("run_pyronaut_process")); - assertTrue(result.contains("run_pyronaut_cli")); - assertTrue(result.contains("test_args+=(--local-repository \"$(pyronaut_dependency_cache)\")")); - assertTrue(result.contains("run_pyronaut_cli test \"${test_args[@]}\"")); - assertFalse(result.contains("\"$PYRONAUT_TEST_EXECUTABLE\"")); - assertTrue(result.contains("set_pyronaut_local_repositories")); + assertTrue(result.contains("set_pyronaut_test_resources &&")); + for (String command : List.of("install", "validate-config", "process", "test")) { + assertTrue(result.contains("pyronaut " + command)); + } + assertFalse(result.contains("PYRONAUT_LOCAL_REPOSITORY")); + assertFalse(result.contains("PYRONAUT_INSTALL_EXECUTABLE")); + assertFalse(result.contains("pyronaut_native_toolchain")); + assertFalse(result.contains("local tmp_dir")); assertTrue(result.contains("cd python-guide-pyronaut-python")); assertFalse(result.contains("}cd python-guide-pyronaut-python")); assertTrue(result.contains("run_pyronaut_tests || EXIT_STATUS=$?")); assertFalse(result.contains("pyenv")); - assertTrue(result.contains("tar --exclude='./.micronaut'")); assertFalse(result.contains("./gradlew -q check")); } diff --git a/gradle/local-composite-builds.gradle b/gradle/local-composite-builds.gradle deleted file mode 100644 index c4d419495f2..00000000000 --- a/gradle/local-composite-builds.gradle +++ /dev/null @@ -1,151 +0,0 @@ -def micronautCoreModules = [ - 'micronaut-aop', - 'micronaut-buffer-netty', - 'micronaut-context', - 'micronaut-context-propagation', - 'micronaut-context-python', - 'micronaut-core', - 'micronaut-core-bom', - 'micronaut-core-processor', - 'micronaut-core-reactive', - 'micronaut-discovery-core', - 'micronaut-function', - 'micronaut-function-client', - 'micronaut-function-web', - 'micronaut-graal', - 'micronaut-http', - 'micronaut-http-client', - 'micronaut-http-client-core', - 'micronaut-http-client-jdk', - 'micronaut-http-netty', - 'micronaut-http-netty-http3', - 'micronaut-http-server', - 'micronaut-http-server-netty', - 'micronaut-http-validation', - 'micronaut-inject', - 'micronaut-inject-groovy', - 'micronaut-inject-java', - 'micronaut-inject-kotlin', - 'micronaut-inject-python', - 'micronaut-jackson-core', - 'micronaut-jackson-databind', - 'micronaut-json-core', - 'micronaut-management', - 'micronaut-messaging', - 'micronaut-module-info', - 'micronaut-module-info-runtime', - 'micronaut-retry', - 'micronaut-router', - 'micronaut-runtime', - 'micronaut-runtime-osx', - 'micronaut-websocket' -] - -def micronautStarterModules = [ - 'micronaut-starter-core': ':starter-core', - 'micronaut-starter-api': ':starter-api', - 'micronaut-starter-aws-cdk': ':starter-aws-cdk', - 'micronaut-starter-aws-lambda': ':starter-aws-lambda', - 'micronaut-starter-azure-function': ':starter-azure-function', - 'micronaut-starter-gcp-function': ':starter-gcp-function', - 'micronaut-starter-web-netty': ':starter-web-netty', - 'micronaut-starter-web-servlet': ':starter-web-servlet', - 'micronaut-cli': ':micronaut-cli' -] - -def pyronautModules = [ - 'micronaut-pyronaut': ':micronaut-pyronaut', - 'micronaut-pyronaut-bom': ':micronaut-pyronaut-bom', - 'micronaut-pyronaut-config-model': ':micronaut-pyronaut-config-model', - 'micronaut-pyronaut-install': ':micronaut-pyronaut-install', - 'micronaut-pyronaut-logging': ':micronaut-pyronaut-logging', - 'micronaut-pyronaut-logback': ':micronaut-pyronaut-logback', - 'micronaut-pyronaut-native-build': ':micronaut-pyronaut-native-build', - 'micronaut-pyronaut-processor': ':micronaut-pyronaut-processor', - 'micronaut-pyronaut-pytest': ':micronaut-pyronaut-pytest', - 'micronaut-pyronaut-requests': ':micronaut-pyronaut-requests', - 'micronaut-pyronaut-run': ':micronaut-pyronaut-run', - 'micronaut-pyronaut-test': ':micronaut-pyronaut-test', - 'micronaut-pyronaut-test-resources-server': ':micronaut-pyronaut-test-resources-server', - 'micronaut-pyronaut-tui': ':micronaut-pyronaut-tui', - 'micronaut-pyronaut-validate-config': ':micronaut-pyronaut-validate-config' -] - -def localGitPath = { String propertyName, String environmentName -> - providers.gradleProperty(propertyName).orNull ?: providers.environmentVariable(environmentName).orNull -} - -def discoverGraalPyBundleRepository = { File checkoutDir -> - File bundleRoot = new File(checkoutDir, 'checkouts/maven-bundles/graalpy') - if (!bundleRoot.isDirectory()) { - return null - } - List candidates = bundleRoot.listFiles() - ?.findAll { File file -> - file.isDirectory() && - new File(file, 'org/graalvm/python/python').exists() && - new File(file, 'org/graalvm/python/python-embedding').exists() - } - ?.sort { File left, File right -> right.lastModified() <=> left.lastModified() } ?: [] - candidates.isEmpty() ? null : candidates.first().absolutePath -} - -def isUsableGraalPyBundleRepository = { String repository -> - repository != null && - new File(repository, 'org/graalvm/python/python').exists() && - new File(repository, 'org/graalvm/python/python-embedding').exists() -} - -def configureGraalPyBundleRepository = { File checkoutDir -> - if (isUsableGraalPyBundleRepository(System.getProperty('micronaut.graalpy.bundle.repo'))) { - return - } - String repository = discoverGraalPyBundleRepository(checkoutDir) - if (repository != null) { - System.setProperty('micronaut.graalpy.bundle.repo', repository) - System.setProperty('pyronaut.graalpy.bundle.repo', repository) - System.setProperty('org.gradle.project.micronaut.graalpy.bundle.repo', repository) - System.setProperty('org.gradle.project.pyronaut.graalpy.bundle.repo', repository) - } -} - -if (providers.gradleProperty('micronaut.include.graalpy.extensions').orNull == null && - System.getProperty('org.gradle.project.micronaut.include.graalpy.extensions') == null) { - System.setProperty('org.gradle.project.micronaut.include.graalpy.extensions', 'false') -} - -def includeLocalBuild = { String buildName, String group, File directory, Map modules -> - if (directory?.isDirectory() && new File(directory, '.git').exists()) { - includeBuild(directory) { - name = buildName - dependencySubstitution { - modules.each { artifactId, projectPath -> - substitute(module("${group}:${artifactId}")).using(project(projectPath)) - } - } - } - } -} - -def localMicronautCore = localGitPath('local.git.micronaut-core', 'LOCAL_GIT_MICRONAUT_CORE') -File localMicronautCoreDir = localMicronautCore ? file(localMicronautCore) : null -if (localMicronautCoreDir != null) { - configureGraalPyBundleRepository(localMicronautCoreDir) -} -includeLocalBuild( - 'micronaut-core', - 'io.micronaut', - localMicronautCoreDir, - micronautCoreModules.collectEntries { [(it): ":${it}"] } -) - -def localPyronaut = localGitPath('local.git.pyronaut', 'LOCAL_GIT_PYRONAUT') -File localPyronautDir = localPyronaut ? file(localPyronaut) : null -if (localPyronautDir != null) { - configureGraalPyBundleRepository(localPyronautDir) -} -includeLocalBuild('pyronaut', 'io.micronaut.pyronaut', localPyronautDir, pyronautModules) - -def localMicronautStarter = localGitPath('local.git.micronaut-starter', 'LOCAL_GIT_MICRONAUT_STARTER') -File localMicronautStarterDir = localMicronautStarter ? file(localMicronautStarter) : null -includeLocalBuild('micronaut-starter', 'io.micronaut.starter', localMicronautStarterDir, micronautStarterModules) diff --git a/settings.gradle b/settings.gradle index fdf2928e085..1cb3bcef89d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,5 +11,3 @@ plugins { rootProject.name = "micronaut-guides" include 'cli' - -apply from: file('gradle/local-composite-builds.gradle') diff --git a/src/docs/common/snippets/common-enable-distributed-configuration.adoc b/src/docs/common/snippets/common-enable-distributed-configuration.adoc index 37964755790..1740b7ab4ce 100644 --- a/src/docs/common/snippets/common-enable-distributed-configuration.adoc +++ b/src/docs/common/snippets/common-enable-distributed-configuration.adoc @@ -1,21 +1,5 @@ === Enable Distributed Configuration -:only-for-languages:python - -Use Micronaut's native configuration-import integration in `config/application-cloud.toml`: - -resource:application-cloud.toml[tag=distributedconfiguration] - -<1> Select the Micronaut secrets-manager integration. -<2> Identify the remote configuration source. -<3> Set the project identifier or authentication mode as described below. - -Do not create `bootstrap.properties` or `bootstrap.toml`. Keep ordinary settings in `config/application.toml` and activate the `cloud` environment when you want to load remote secrets. Local tests use the `test` environment and do not contact the cloud service. A local database test does not verify access to your remote secrets. - -:only-for-languages: - -:exclude-for-languages:python - Create a `bootstrap.properties` file in the `resources` directory to https://docs.micronaut.io/latest/guide/#bootstrap[enable distributed configuration]. Add the following: @@ -43,6 +27,4 @@ You can disable distributed configuration in a test by annotating a test with: ---- @Property(name = "micronaut.config-client.enabled", value = StringUtils.FALSE) @MicronautTest ----- - -:exclude-for-languages: +---- \ No newline at end of file
${javaImg}${kotlinImg}${groovyImg}${pythonImg}${pythonImg}
${pyronautImg}Read