├── .editorconfig ├── .gitattributes ├── .github ├── actions │ └── build │ │ └── action.yml ├── link-check-config.json ├── renovate.json5 ├── scripts │ ├── replace_string.py │ ├── requirements.txt │ ├── test_replace_string.py │ ├── test_resources │ │ └── api_manual.html │ ├── test_update_api_spec_version.py │ └── update_api_spec_version.py └── workflows │ ├── pr.yml │ ├── publish-javadoc.yml │ ├── publish-library.yml │ ├── submit-dependencies.yml │ ├── test-readme-links.yml │ ├── update-api-spec.yml │ └── update-examples.yml ├── .gitignore ├── LICENSE ├── README.md ├── build-logic ├── build.gradle.kts ├── gradle.properties ├── settings.gradle.kts └── src │ ├── functionalTest │ └── kotlin │ │ └── com │ │ └── gabrielfeo │ │ └── task │ │ └── PostProcessGeneratedApiTest.kt │ └── main │ └── kotlin │ └── com │ └── gabrielfeo │ ├── develocity-api-code-generation.gradle.kts │ ├── examples-test-suite.gradle.kts │ ├── integration-test-suite.gradle.kts │ ├── kotlin-jvm-library.gradle.kts │ ├── no-op.gradle.kts │ ├── published-kotlin-jvm-library.gradle.kts │ ├── task │ └── PostProcessGeneratedApi.kt │ └── test-fixtures.gradle.kts ├── build.gradle.kts ├── docs ├── AccessKeys.md ├── Logging.md └── media │ ├── AccessPage.png │ ├── AnonymousAccessPage.png │ ├── IntelliJKernelLogs.png │ └── IntelliJKernelSettings.png ├── examples ├── example-gradle-task │ ├── README.md │ ├── build.gradle.kts │ ├── buildSrc │ │ ├── build.gradle.kts │ │ ├── settings.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── kotlin │ │ │ └── build │ │ │ └── logic │ │ │ ├── DevelocityApiService.kt │ │ │ ├── PerformanceMetricsTask.kt │ │ │ └── performance-metrics-plugin.gradle.kts │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle.kts ├── example-notebooks │ ├── Logging.ipynb │ ├── MostFrequentBuilds.ipynb │ └── requirements.txt ├── example-project │ ├── build.gradle.kts │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── gabrielfeo │ │ └── develocity │ │ └── api │ │ └── example │ │ ├── Main.kt │ │ └── analysis │ │ └── MostFrequentBuilds.kt └── example-scripts │ └── example-script.main.kts ├── gradle.properties ├── gradle ├── gradle-daemon-jvm.properties ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .openapi-generator-ignore ├── api │ └── library.api ├── build.gradle.kts └── src │ ├── examplesTest │ ├── kotlin │ │ └── com │ │ │ └── gabrielfeo │ │ │ └── develocity │ │ │ └── api │ │ │ └── example │ │ │ ├── JsonParser.kt │ │ │ ├── Queries.kt │ │ │ ├── Shell.kt │ │ │ ├── gradle │ │ │ ├── ExampleGradleTaskTest.kt │ │ │ ├── ExampleProjectTest.kt │ │ │ └── ResourceInitScripts.kt │ │ │ ├── notebook │ │ │ ├── Jupyter.kt │ │ │ ├── NotebookJson.kt │ │ │ ├── NotebooksTest.kt │ │ │ └── PythonVenv.kt │ │ │ └── script │ │ │ └── ScriptsTest.kt │ └── resources │ │ ├── force-snapshot-library.init.gradle.kts │ │ ├── preprocessors.py │ │ └── require-java-11-compatibility.init.gradle │ ├── integrationTest │ ├── kotlin │ │ └── com │ │ │ └── gabrielfeo │ │ │ └── develocity │ │ │ └── api │ │ │ ├── DevelocityApiIntegrationTest.kt │ │ │ ├── InMemoryLogRecorder.kt │ │ │ ├── LibraryApiNamingTest.kt │ │ │ ├── LoggingIntegrationTest.kt │ │ │ ├── SmokeTest.kt │ │ │ ├── extension │ │ │ ├── BuildsApiExtensionsIntegrationTest.kt │ │ │ └── RequestRecorder.kt │ │ │ └── internal │ │ │ └── jupyter │ │ │ └── DevelocityApiJupyterIntegrationTest.kt │ └── resources │ │ ├── logback-test.xml │ │ └── response │ │ └── api │ │ └── builds │ │ └── 5-builds.json │ ├── main │ ├── kotlin │ │ └── com │ │ │ └── gabrielfeo │ │ │ └── develocity │ │ │ └── api │ │ │ ├── Config.kt │ │ │ ├── DevelocityApi.kt │ │ │ ├── extension │ │ │ ├── BuildAttributesValueExtensions.kt │ │ │ ├── BuildsApiExtensions.kt │ │ │ └── Mapping.kt │ │ │ └── internal │ │ │ ├── ApiConstants.kt │ │ │ ├── Env.kt │ │ │ ├── OkHttpClient.kt │ │ │ ├── OkHttpClientBuilderFactory.kt │ │ │ ├── Retrofit.kt │ │ │ ├── SystemProperties.kt │ │ │ ├── auth │ │ │ ├── AccessKeyResolver.kt │ │ │ └── HostAccessKeyEntry.kt │ │ │ ├── caching │ │ │ ├── CacheEnforcingInterceptor.kt │ │ │ └── CacheHitLoggingInterceptor.kt │ │ │ └── jupyter │ │ │ └── DevelocityApiJupyterIntegration.kt │ └── resources │ │ └── META-INF │ │ └── kotlin-jupyter-libraries │ │ └── libraries.json │ ├── test │ ├── kotlin │ │ └── com │ │ │ └── gabrielfeo │ │ │ └── develocity │ │ │ └── api │ │ │ ├── CacheConfigTest.kt │ │ │ ├── ConfigTest.kt │ │ │ ├── DevelocityApiExtensionsTest.kt │ │ │ ├── DevelocityApiTest.kt │ │ │ ├── FakeBuildsApi.kt │ │ │ ├── OkHttpClientTest.kt │ │ │ ├── RetrofitTest.kt │ │ │ ├── TestResourceUtils.kt │ │ │ ├── extension │ │ │ ├── BuildsApiExtensionsTest.kt │ │ │ └── MappingTest.kt │ │ │ ├── internal │ │ │ ├── auth │ │ │ │ └── AccessKeyResolverTest.kt │ │ │ └── caching │ │ │ │ └── CacheEnforcingInterceptorTest.kt │ │ │ └── model │ │ │ ├── BuildAttributesValueExtensionsTest.kt │ │ │ └── FakeBuild.kt │ └── resources │ │ └── gradle-attributes-response.json │ └── testFixtures │ └── kotlin │ └── com │ └── gabrielfeo │ └── develocity │ └── api │ ├── FakeDevelocityApiScaffold.kt │ ├── Resources.kt │ └── internal │ ├── FakeEnv.kt │ └── FakeSystemProperties.kt └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.kt, *.kts] 4 | indent_style = space 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | description: 'Run a Gradle build' 3 | inputs: 4 | args: 5 | description: "Gradle args" 6 | required: false 7 | artifact-name: 8 | description: "Artifact name" 9 | required: false 10 | path-to-upload: 11 | description: "Path to upload as artifact" 12 | required: false 13 | dry-run: 14 | description: "Whether to --dry-run" 15 | type: boolean 16 | default: false 17 | runs: 18 | using: "composite" 19 | steps: 20 | - name: Set up Java 21 | uses: actions/setup-java@v5 22 | with: 23 | distribution: liberica 24 | java-version: | 25 | 24 26 | 17 27 | - name: Setup Python 28 | uses: actions/setup-python@v6 29 | with: 30 | python-version: '3.14' 31 | cache: 'pip' 32 | - name: Set up Gradle 33 | uses: gradle/actions/setup-gradle@v5 34 | with: 35 | validate-wrappers: true 36 | add-job-summary-as-pr-comment: 'on-failure' 37 | # Disable cache for releases, disable write for bot branches, enable write everywhere else 38 | cache-disabled: ${{ startsWith('refs/tags', github.ref) }} 39 | cache-read-only: ${{ contains('renovate/', github.ref) || contains('bot/', github.ref) }} 40 | - name: Run Gradle 41 | shell: bash 42 | run: | 43 | if [[ "${{ inputs.dry-run }}" == "true" ]]; then 44 | ./gradlew --dry-run ${{ inputs.args }} 45 | else 46 | ./gradlew ${{ inputs.args }} 47 | fi 48 | - name: Upload 49 | if: ${{ inputs.path-to-upload }} 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: ${{ inputs.artifact-name }} 53 | path: ${{ inputs.path-to-upload }} 54 | if-no-files-found: warn 55 | -------------------------------------------------------------------------------- /.github/link-check-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "retryCount": 3, 3 | "fallbackRetryDelay": "120s", 4 | "replacementPatterns": [ 5 | { 6 | "pattern": "^https://nbviewer.org/github/", 7 | "replacement": "https://github.com/" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ], 6 | "schedule": [ 7 | // Runs once, despite '*' minutes (https://docs.renovatebot.com/configuration-options/#schedule) 8 | "* 0-6 * * 1", 9 | ], 10 | "ignorePaths": [ 11 | "**/test_resources/**", 12 | ], 13 | "dependencyDashboard": true, 14 | "branchConcurrentLimit": 0, 15 | "prConcurrentLimit": 0, 16 | "prHourlyLimit": 0, 17 | "rebaseWhen": "behind-base-branch", 18 | // Remove configDescription from PR body 19 | "prBodyTemplate": "{{{header}}}{{{table}}}{{{warnings}}}{{{notes}}}{{{changelogs}}}{{{controls}}}{{{footer}}}", 20 | // Remove "dependency " prefix which makes message subjects too long 21 | "commitMessageTopic": "{{depName}}", 22 | "packageRules": [ 23 | // Add a changelog link to Develocity plugin PRs 24 | { 25 | "matchDepNames": ["com.gradle.develocity"], 26 | "prBodyNotes": ["https://docs.gradle.com/develocity/gradle-plugin/current#release_history"], 27 | }, 28 | // Group Kotlin/Jupyter artifact bumps 29 | { 30 | "matchDepNames": [ 31 | "org.jetbrains.kotlinx:kotlin-jupyter-test-kit", 32 | "org.jetbrains.kotlinx:kotlin-jupyter-api", 33 | // Two possible names for the plugin (GAV or plugin ID) 34 | "org.jetbrains.kotlinx:kotlin-jupyter-api-gradle-plugin", 35 | "org.jetbrains.kotlin.jupyter.api", 36 | ], 37 | "groupName": "Kotlin/Jupyter", 38 | "groupSlug": "kotlin-jupyter", 39 | }, 40 | // Group bumps of .github/scripts dependencies 41 | { 42 | "matchFileNames": [".github/scripts/**"], 43 | "groupName": ".github/scripts dependencies", 44 | }, 45 | // Group bumps of examples/ dependencies 46 | { 47 | "matchFileNames": ["examples/**"], 48 | "groupName": "examples dependencies", 49 | }, 50 | ], 51 | } 52 | -------------------------------------------------------------------------------- /.github/scripts/replace_string.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # pylint: disable=missing-function-docstring,missing-module-docstring 3 | 4 | import argparse 5 | from pathlib import Path 6 | import re 7 | import sys 8 | import git 9 | 10 | 11 | def main() -> None: 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument("path", type=Path, help="Path to root directory") 14 | parser.add_argument("old", type=str, help="Old string to be replaced") 15 | parser.add_argument( 16 | "new", type=str, help="New string to replace old string") 17 | args = parser.parse_args() 18 | replace_string(args.path, args.old, args.new) 19 | 20 | 21 | def replace_string(path: Path, old: str, new: str) -> None: 22 | repo = git.Repo(path, search_parent_directories=True) 23 | print(f'Replacing {old} for {new}...') 24 | for file in path.glob('**/*'): 25 | if not _should_replace_in(repo, file): 26 | continue 27 | try: 28 | text = file.read_text() 29 | text = _replace_badge_versions(text, old, new) 30 | text = _replace_non_badge_versions(text, old, new) 31 | file.write_text(text) 32 | print(f'Replaced in file {file}') 33 | except UnicodeError as e: 34 | print(f'Error processing file {file}:', e, file=sys.stderr) 35 | 36 | 37 | def _replace_badge_versions(text: str, old: str, new: str) -> str: 38 | return re.sub( 39 | rf'''https://img\.shields\.io/badge/(.+?)-{_badge_version(old)}-(\w+)''', 40 | rf'''https://img.shields.io/badge/\1-{_badge_version(new)}-\2''', 41 | text 42 | ) 43 | 44 | 45 | def _replace_non_badge_versions(text: str, old: str, new: str) -> str: 46 | return re.sub(rf'(?!https://img\.shields\.io/badge/){old}', new, text) 47 | 48 | 49 | def _badge_version(version: str) -> str: 50 | return version.replace('-', '--') 51 | 52 | 53 | def _should_replace_in(repo, file: Path) -> bool: 54 | return file.is_file() \ 55 | and not repo.ignored(file) \ 56 | and file.parts[0] != '.git' \ 57 | and file.name != 'test_replace_string.py' 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /.github/scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2025.10.5 2 | charset-normalizer==3.4.4 3 | gitdb==4.0.12 4 | GitPython==3.1.45 5 | idna==3.11 6 | requests==2.32.5 7 | smmap==5.0.2 8 | urllib3==2.5.0 9 | -------------------------------------------------------------------------------- /.github/scripts/test_replace_string.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # pylint: disable=missing-function-docstring,missing-module-docstring,missing-class-docstring 3 | 4 | from textwrap import dedent 5 | import unittest 6 | from unittest import mock 7 | from pathlib import Path 8 | from tempfile import TemporaryDirectory 9 | from replace_string import replace_string 10 | 11 | class TestReplaceString(unittest.TestCase): 12 | 13 | @mock.patch('git.Repo') 14 | def test_stable_to_stable(self, repo): 15 | self._test_replace_string(repo, [ 16 | ('2024.1.0', '2024.2.0'), 17 | ('2024.1.0', '2024.1.1'), 18 | ('2024.4.2', '2025.1.0') 19 | ]) 20 | 21 | @mock.patch('git.Repo') 22 | def test_stable_to_pre_release(self, repo): 23 | self._test_replace_string(repo, [ 24 | ('2024.1.0', '2024.1.1-alpha01'), 25 | ('2024.1.0', '2024.1.1-beta01'), 26 | ('2024.1.0', '2024.2.0-alpha01'), 27 | ('2024.1.0', '2024.2.0-beta01'), 28 | ('2024.4.2', '2025.1.0-alpha01'), 29 | ]) 30 | 31 | @mock.patch('git.Repo') 32 | def test_pre_release_to_stable(self, repo): 33 | self._test_replace_string(repo, [ 34 | ('2024.1.0-alpha01', '2024.1.0'), 35 | ('2024.1.0-beta01', '2024.1.0'), 36 | ('2024.1.0-rc01', '2024.1.0'), 37 | ]) 38 | 39 | @mock.patch('git.Repo') 40 | def test_pre_release_to_pre_release(self, repo): 41 | self._test_replace_string(repo, [ 42 | ('2024.1.0-alpha01', '2024.1.0-alpha02'), 43 | ('2024.1.0-alpha02', '2024.1.0-beta01'), 44 | ('2024.1.0-beta01', '2024.1.1-rc01'), 45 | ]) 46 | 47 | def _test_replace_string(self, repo, replacements): 48 | repo.return_value.ignored.return_value = False 49 | for old, new in replacements: 50 | with TemporaryDirectory() as temp_dir: 51 | target = Path(temp_dir) 52 | write_test_files(target, old) 53 | replace_string(target, old, new) 54 | for file in target.glob('**/*'): 55 | self._assert_replaced(target / file, old, new) 56 | 57 | def _assert_replaced(self, file, old, new): 58 | content = file.read_text() 59 | self._assert_regular_versions_replaced(content, old, new) 60 | self._assert_badge_versions_replaced(content, old, new) 61 | 62 | def _assert_regular_versions_replaced(self, content, old, new): 63 | self.assertIn(new, content) 64 | self.assertNotIn(old, content) 65 | 66 | def _assert_badge_versions_replaced(self, content, old, new): 67 | if "badge/" not in content: 68 | return 69 | self.assertIn( 70 | f"badge/Maven%20Central-{new.replace('-', '--')}-blue", content) 71 | self.assertNotIn( 72 | f"badge/Maven%20Central-{old.replace('-', '--')}-blue", content) 73 | 74 | 75 | def write_test_files(target_dir, old): 76 | (target_dir / 'README.md').write_text(dedent(f''' 77 | # Title 78 | 79 | [![Maven Central](https://img.shields.io/badge/Maven%20Central-{old.replace('-', '--')}-blue)][14] 80 | 81 | ```kotlin 82 | @file:DependsOn("com.gabrielfeo:develocity-api-kotlin:{old}") 83 | implementation("com.gabrielfeo:develocity-api-kotlin:{old}") 84 | ``` 85 | 86 | ``` 87 | %use develocity-api-kotlin(version={old}) 88 | ``` 89 | 90 | [14]: https://central.sonatype.com/artifact/com.gabrielfeo/develocity-api-kotlin/{old} 91 | ''')) 92 | (target_dir / 'build.gradle.kts').write_text(dedent(f''' 93 | dependencies {'{'} 94 | implementation("com.gabrielfeo:develocity-api-kotlin:{old}") 95 | {'}'} 96 | ''')) 97 | (target_dir / 'notebook.ipynb').write_text(dedent(''' 98 | { 99 | "metadata": {}, 100 | "nbformat": 4, 101 | "nbformat_minor": 2, 102 | "cells": [ 103 | { 104 | "cell_type": "code", 105 | "execution_count": 1, 106 | "source": [ 107 | "%use gradle-enterprise-api-kotlin(version={{VERSION}})" 108 | ], 109 | "outputs": [] 110 | } 111 | ] 112 | } 113 | ''').replace('{{VERSION}}', old)) 114 | 115 | 116 | if __name__ == "__main__": 117 | unittest.main() 118 | -------------------------------------------------------------------------------- /.github/scripts/test_update_api_spec_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pathlib import Path 4 | from update_api_spec_version import main 5 | from tempfile import NamedTemporaryFile 6 | import unittest 7 | from unittest import mock 8 | 9 | TEST_RESOURCES = Path(__file__).parent / 'test_resources' 10 | TEST_API_MANUAL_HTML = TEST_RESOURCES / 'api_manual.html' 11 | LATEST_VERSION = '2024.1' # Same as HTML 12 | 13 | 14 | class TestCheckForNewApiSpec(unittest.TestCase): 15 | 16 | @mock.patch('builtins.print') 17 | @mock.patch('requests.get') 18 | def test_main_with_update_available(self, mock_get, _): 19 | mock_get.return_value.status_code = 200 20 | mock_get.return_value.text = TEST_API_MANUAL_HTML.read_text() 21 | with self.properties_file(version='2022.4') as file: 22 | main(properties_file=file.name) 23 | self.assert_properties_version(file, LATEST_VERSION) 24 | 25 | @mock.patch('builtins.print') 26 | @mock.patch('requests.get') 27 | def test_main_without_update_available(self, mock_get, _): 28 | mock_get.return_value.status_code = 200 29 | mock_get.return_value.text = TEST_API_MANUAL_HTML.read_text() 30 | with self.properties_file(version=LATEST_VERSION) as file: 31 | with self.assertRaises(SystemExit): 32 | main(properties_file=file.name) 33 | self.assert_properties_version(file, LATEST_VERSION) 34 | 35 | def assert_properties_version(self, file, version): 36 | with open(file.name) as file: 37 | expected = f"develocity.version={version}\nversion={version}.0\n1=2\n" 38 | self.assertEqual(file.read(), expected) 39 | 40 | def properties_file(self, version): 41 | file = NamedTemporaryFile() 42 | content = f"develocity.version={version}\nversion={version}.0\n1=2\n" 43 | file.write(content.encode()) 44 | file.flush() 45 | return file 46 | 47 | 48 | if __name__ == '__main__': 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /.github/scripts/update_api_spec_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests 4 | import re 5 | import fileinput 6 | import sys 7 | 8 | VERSIONS_URL = "https://docs.gradle.com/enterprise/api-manual/" 9 | LATEST_VERSION_REGEX = r']*href="ref/develocity-([\d.]+)-api\.yaml">Specification' 10 | 11 | 12 | def main(properties_file='gradle.properties'): 13 | current = get_current_api_spec_version(properties_file) 14 | print(f"Current spec version in", properties_file, "is", current, file=sys.stderr) 15 | latest = extract_latest_version() 16 | if current == latest: 17 | exit(1) 18 | update_version(properties_file, latest) 19 | print(latest) 20 | 21 | 22 | def get_current_api_spec_version(properties_file) -> str: 23 | with open(properties_file, mode='r') as file: 24 | for line in file.readlines(): 25 | if '=' not in line: 26 | continue 27 | k, v = line.strip().split('=', maxsplit=2) 28 | if k == 'develocity.version': 29 | return v 30 | 31 | 32 | def extract_latest_version(): 33 | resp = requests.get(VERSIONS_URL) 34 | resp.raise_for_status() 35 | match = re.search(LATEST_VERSION_REGEX, resp.text) 36 | if not match: 37 | raise RuntimeError(f"Failed to get latest version from {VERSIONS_URL} \ 38 | with /{LATEST_VERSION_REGEX}/") 39 | print("First match in HTML of ", VERSIONS_URL, "is", match, file=sys.stderr) 40 | return match.group(1) 41 | 42 | 43 | def update_version(properties_file, new_version): 44 | for line in fileinput.input(properties_file, inplace=True): 45 | if '=' in line: 46 | k, v = line.strip().split('=', maxsplit=2) 47 | # Update target API spec version 48 | if k == 'develocity.version': 49 | line = f"{k}={new_version}\n" 50 | # Update library version 51 | if k == 'version': 52 | line = f"{k}={new_version}.0\n" 53 | sys.stdout.write(line) 54 | 55 | 56 | if __name__ == '__main__': 57 | main() 58 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: 'Check PR' 2 | 3 | on: 4 | pull_request 5 | 6 | defaults: 7 | run: 8 | shell: bash 9 | 10 | jobs: 11 | 12 | kotlin-tests: 13 | runs-on: ubuntu-latest 14 | env: 15 | DEVELOCITY_URL: "${{ vars.DEVELOCITY_URL }}" 16 | DEVELOCITY_ACCESS_KEY: "${{ secrets.DEVELOCITY_ACCESS_KEY }}" 17 | DEVELOCITY_API_CACHE_ENABLED: "false" 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v5 21 | - name: gradle check 22 | uses: ./.github/actions/build 23 | with: 24 | args: check 25 | 26 | python-tests: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v5 31 | - name: Setup Python 32 | uses: actions/setup-python@v6 33 | with: 34 | python-version: '3.14' 35 | cache: 'pip' 36 | - run: pip install -r .github/scripts/requirements.txt 37 | - name: 'unittest discover' 38 | run: python3 -m unittest discover -bs .github/scripts 39 | 40 | readme-links-test: 41 | uses: ./.github/workflows/test-readme-links.yml 42 | 43 | dry-run-publish-javadoc: 44 | uses: ./.github/workflows/publish-javadoc.yml 45 | with: 46 | dry_run: true 47 | secrets: inherit 48 | 49 | dry-run-publish-library: 50 | uses: ./.github/workflows/publish-library.yml 51 | with: 52 | dry_run: true 53 | secrets: inherit 54 | 55 | dry-run-update-api-spec: 56 | uses: ./.github/workflows/update-api-spec.yml 57 | with: 58 | dry_run: true 59 | -------------------------------------------------------------------------------- /.github/workflows/publish-javadoc.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish javadoc' 2 | 3 | on: 4 | push: 5 | tags: ['*'] 6 | workflow_dispatch: 7 | inputs: 8 | dry_run: 9 | description: 'Dry run' 10 | type: boolean 11 | default: false 12 | workflow_call: 13 | inputs: 14 | dry_run: 15 | description: 'Dry run' 16 | type: boolean 17 | default: false 18 | 19 | defaults: 20 | run: 21 | shell: bash 22 | 23 | jobs: 24 | 25 | build-javadoc: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v5 30 | - name: Build javadoc 31 | uses: ./.github/actions/build 32 | with: 33 | args: >- 34 | dokkaGenerate 35 | '-Pversion=${{ github.ref_name }}' 36 | artifact-name: 'docs' 37 | path-to-upload: "library/build/dokka/html/**/*" 38 | 39 | publish-javadoc: 40 | needs: [build-javadoc] 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: write 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v5 47 | with: 48 | ref: gh-pages 49 | - name: Delete current javadoc 50 | run: rm -rf docs 51 | - name: Download new javadoc 52 | uses: actions/download-artifact@v5 53 | with: 54 | path: docs 55 | - name: Commit 56 | run: | 57 | git config user.name github-actions 58 | git config user.email github-actions@github.com 59 | git add docs 60 | git commit --allow-empty -m "Add ${{ github.ref_name }} javadoc" 61 | - name: Push 62 | run: | 63 | if [[ "${{ inputs.dry_run }}" == 'true' ]]; then 64 | args=' --dry-run' 65 | fi 66 | git push $args 67 | -------------------------------------------------------------------------------- /.github/workflows/publish-library.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish library to Central' 2 | 3 | on: 4 | push: 5 | tags: ['*'] 6 | workflow_dispatch: 7 | inputs: 8 | dry_run: 9 | description: 'Dry run' 10 | type: boolean 11 | default: false 12 | workflow_call: 13 | inputs: 14 | dry_run: 15 | description: 'Dry run' 16 | type: boolean 17 | default: false 18 | 19 | defaults: 20 | run: 21 | shell: bash 22 | 23 | jobs: 24 | 25 | build-and-publish: 26 | runs-on: ubuntu-latest 27 | env: 28 | DEVELOCITY_URL: "${{ vars.DEVELOCITY_URL }}" 29 | DEVELOCITY_ACCESS_KEY: "${{ secrets.DEVELOCITY_ACCESS_KEY }}" 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v5 33 | - name: Verify that version property matches tag 34 | if: ${{ inputs.dry_run != true }} 35 | run: grep -qP '^version=${{ github.ref_name }}$' gradle.properties 36 | - name: gradle publish 37 | uses: ./.github/actions/build 38 | with: 39 | dry-run: ${{ inputs.dry_run }} 40 | args: >- 41 | check 42 | publishDevelocityApiKotlinPublicationToMavenCentralRepository 43 | publishRelocationPublicationToMavenCentralRepository 44 | --rerun-tasks 45 | '-PmavenCentralUsername=${{ secrets.MAVEN_CENTRAL_USERNAME }}' 46 | '-PmavenCentralPassword=${{ secrets.MAVEN_CENTRAL_PASSWORD }}' 47 | '-Psigning.password=${{ secrets.GPG_PASSWORD }}' 48 | '-Psigning.secretKey=${{ secrets.GPG_SECRET_KEY }}' 49 | artifact-name: 'outputs' 50 | path-to-upload: | 51 | library/build/*-api.yaml 52 | library/build/post-processed-api/**/* 53 | library/build/publications/**/* 54 | library/build/libs/**/* 55 | -------------------------------------------------------------------------------- /.github/workflows/submit-dependencies.yml: -------------------------------------------------------------------------------- 1 | name: 'Submit dependencies' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_call: 8 | workflow_dispatch: 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | 16 | submit-dependencies: 17 | runs-on: ubuntu-latest 18 | permissions: # The Dependency Submission API requires write permission 19 | contents: write 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v5 23 | - name: Submit Gradle dependencies 24 | uses: gradle/actions/dependency-submission@v5 25 | env: 26 | DEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS: '.*[Tt]est(Compile|Runtime)Classpath' 27 | -------------------------------------------------------------------------------- /.github/workflows/test-readme-links.yml: -------------------------------------------------------------------------------- 1 | name: 'Test README links' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - gh-pages 8 | workflow_call: 9 | workflow_dispatch: 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | jobs: 16 | 17 | test-readme-links: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v5 22 | - name: Check README links 23 | uses: gaurav-nelson/github-action-markdown-link-check@v1 24 | with: 25 | folder-path: . 26 | max-depth: 1 27 | use-verbose-mode: yes 28 | config-file: .github/link-check-config.json 29 | -------------------------------------------------------------------------------- /.github/workflows/update-api-spec.yml: -------------------------------------------------------------------------------- 1 | name: 'Update API spec' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 6 * * *' 6 | workflow_dispatch: 7 | inputs: 8 | dry_run: 9 | description: 'Dry run' 10 | type: boolean 11 | default: false 12 | workflow_call: 13 | inputs: 14 | dry_run: 15 | description: 'Dry run' 16 | type: boolean 17 | default: false 18 | 19 | defaults: 20 | run: 21 | shell: bash 22 | 23 | jobs: 24 | 25 | update-api-spec: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: 'Checkout' 29 | uses: actions/checkout@v5 30 | - name: 'Update API spec version' 31 | run: | 32 | if ./.github/scripts/update_api_spec_version.py | tee new.txt; then 33 | echo "NEW_VERSION=$(cat new.txt)" >> $GITHUB_ENV 34 | fi 35 | rm new.txt || true 36 | - name: gradle :library:apiDump 37 | uses: ./.github/actions/build 38 | with: 39 | dry-run: ${{ inputs.dry_run }} 40 | args: :library:apiDump 41 | - name: Check for existing PR 42 | if: ${{ env.NEW_VERSION }} 43 | run: | 44 | set -u 45 | update_branch="feature/api-spec-$NEW_VERSION" 46 | echo "UPDATE_BRANCH=$update_branch" >> $GITHUB_ENV 47 | if git ls-remote --exit-code --heads origin "$update_branch"; then 48 | echo "EXISTING_PR=true" >> $GITHUB_ENV 49 | fi 50 | - name: 'Create PR' 51 | if: ${{ inputs.dry_run != true && env.NEW_VERSION && !env.EXISTING_PR }} 52 | uses: peter-evans/create-pull-request@v7 53 | with: 54 | branch: "${{ env.UPDATE_BRANCH }}" 55 | commit-message: "Bump Develocity API spec version to ${{ env.NEW_VERSION }}" 56 | title: | 57 | Bump Develocity API spec version to ${{ env.NEW_VERSION }} 58 | 59 | Generated from workflow run [${{ github.run_id }}][1]. 60 | 61 | [1]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 62 | body: "https://docs.gradle.com/enterprise/api-manual/#release_history" 63 | author: "github-actions " 64 | committer: "github-actions " 65 | add-paths: "gradle.properties" 66 | delete-branch: true 67 | -------------------------------------------------------------------------------- /.github/workflows/update-examples.yml: -------------------------------------------------------------------------------- 1 | name: 'Update examples' 2 | 3 | on: 4 | push: 5 | tags: [ '*' ] 6 | workflow_dispatch: 7 | inputs: 8 | old: 9 | description: | 10 | git ref of version to be replaced with 'new'. Defaults to the first git tag lower than 'new' in v:refname sorting. 11 | required: false 12 | new: 13 | description: | 14 | git ref of version to replace 'old'. Defaults to the current git ref (github.ref_name). 15 | required: false 16 | 17 | defaults: 18 | run: 19 | shell: bash 20 | 21 | jobs: 22 | 23 | update-examples: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: 'Checkout' 27 | uses: actions/checkout@v5 28 | with: 29 | fetch-depth: 0 30 | - name: Setup Python 31 | uses: actions/setup-python@v6 32 | with: 33 | python-version: '3.14' 34 | cache: 'pip' 35 | - run: pip install -r .github/scripts/requirements.txt 36 | - name: 'Get versions' 37 | run: | 38 | old="${{ inputs.old }}" 39 | new="${{ inputs.new || github.ref_name }}" 40 | echo "NEW_VERSION=$new" | tee -a $GITHUB_ENV 41 | if [[ "$old" == "" ]]; then 42 | echo "Auto-detecting old version from git tags" 43 | # Get previous version from descending tag list 44 | old="$(git tag --sort=-v:refname | grep -A1 "$new" | tail -1)" 45 | fi 46 | echo "OLD_VERSION=$old" | tee -a $GITHUB_ENV 47 | - name: 'Update version in all files' 48 | run: ./.github/scripts/replace_string.py ./ "$OLD_VERSION" "$NEW_VERSION" 49 | - name: 'Create PR' 50 | uses: peter-evans/create-pull-request@v7 51 | with: 52 | base: 'main' 53 | branch: "replace-${{ env.OLD_VERSION }}-${{ env.NEW_VERSION }}" 54 | title: "Bump examples and badges to ${{ env.NEW_VERSION }}" 55 | author: "github-actions " 56 | committer: "github-actions " 57 | body: "Bump versions from ${{ env.OLD_VERSION }} to ${{ env.NEW_VERSION }}." 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .kotlin 3 | out 4 | !*/src/**/out 5 | 6 | gradle-user-home 7 | profile-out* 8 | 9 | build 10 | !.github/actions/build 11 | !**/src/**/build 12 | .ipynb_checkpoints 13 | 14 | .venv 15 | __pycache__ 16 | 17 | **/.log 18 | .DS_Store 19 | 20 | .idea 21 | local.properties 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Gabriel Feo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | } 6 | 7 | java { 8 | toolchain { 9 | languageVersion = JavaLanguageVersion.of(17) 10 | vendor = JvmVendorSpec.AZUL 11 | } 12 | } 13 | 14 | testing { 15 | suites { 16 | register("functionalTest") { 17 | useJUnitJupiter() 18 | } 19 | } 20 | } 21 | 22 | gradlePlugin { 23 | testSourceSets(sourceSets["functionalTest"]) 24 | } 25 | 26 | tasks.named("check") { 27 | dependsOn(tasks.withType()) 28 | } 29 | 30 | dependencies { 31 | implementation(libs.kotlin.plugin) 32 | implementation(libs.kotlin.binary.compatibility.validator.plugin) 33 | implementation(libs.dokka.plugin) 34 | implementation(libs.openapi.generator.plugin) 35 | implementation(libs.vanniktech.mavenPublishPlugin) 36 | "functionalTestImplementation"(project) 37 | } 38 | -------------------------------------------------------------------------------- /build-logic/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.daemon.jvmargs=-Xmx1g 2 | org.gradle.configuration-cache=true 3 | org.gradle.configuration-cache.parallel=true 4 | -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | id("org.gradle.toolchains.foojay-resolver-convention") version("1.0.0") 5 | } 6 | 7 | dependencyResolutionManagement { 8 | repositories { 9 | mavenCentral() 10 | gradlePluginPortal() 11 | } 12 | versionCatalogs { 13 | create("libs") { 14 | from(files("../gradle/libs.versions.toml")) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/com/gabrielfeo/develocity-api-code-generation.gradle.kts: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo 2 | 3 | import com.gabrielfeo.task.PostProcessGeneratedApi 4 | import org.gradle.kotlin.dsl.* 5 | 6 | plugins { 7 | id("com.gabrielfeo.kotlin-jvm-library") 8 | id("org.openapi.generator") 9 | } 10 | 11 | val downloadApiSpec by tasks.registering { 12 | val remoteSpecUrl = providers.gradleProperty("remoteSpecUrl").orElse( 13 | providers.gradleProperty("develocity.version").map { geVersion -> 14 | val majorVersion = geVersion.substringBefore('.').toInt() 15 | val specName = when { 16 | majorVersion <= 2023 -> "gradle-enterprise-$geVersion-api.yaml" 17 | else -> "develocity-$geVersion-api.yaml" 18 | } 19 | "https://docs.gradle.com/enterprise/api-manual/ref/$specName" 20 | } 21 | ) 22 | val spec = resources.text.fromUri(remoteSpecUrl) 23 | val specName = remoteSpecUrl.map { it.substringAfterLast('/') } 24 | val outFile = project.layout.buildDirectory.file(specName) 25 | inputs.property("Spec URL", remoteSpecUrl) 26 | outputs.file(outFile) 27 | doLast { 28 | logger.info("Downloaded API spec from ${remoteSpecUrl.get()}") 29 | spec.asFile().renameTo(outFile.get().asFile) 30 | } 31 | } 32 | 33 | openApiGenerate { 34 | generatorName = "kotlin" 35 | inputSpec = downloadApiSpec.map { it.outputs.files.first().absolutePath } 36 | val generateDir = project.layout.buildDirectory.dir("generated-api") 37 | .map { it.asFile.absolutePath } 38 | outputDir = generateDir 39 | val ignoreFile = project.layout.projectDirectory.file(".openapi-generator-ignore") 40 | ignoreFileOverride.set(ignoreFile.asFile.absolutePath) 41 | apiPackage = "com.gabrielfeo.develocity.api" 42 | modelPackage = "com.gabrielfeo.develocity.api.model" 43 | packageName = "com.gabrielfeo.develocity.api.internal" 44 | invokerPackage = "com.gabrielfeo.develocity.api.internal" 45 | additionalProperties.put("library", "jvm-retrofit2") 46 | additionalProperties.put("useCoroutines", true) 47 | additionalProperties.put("enumPropertyNaming", "camelCase") 48 | additionalProperties.put("useResponseAsReturnType", false) 49 | cleanupOutput = true 50 | } 51 | 52 | val postProcessGeneratedApi by tasks.registering(PostProcessGeneratedApi::class) { 53 | val generatedSrc = tasks.openApiGenerate 54 | .flatMap { it.outputDir } 55 | .map { File(it) } 56 | originalFiles.convention(project.layout.dir(generatedSrc)) 57 | postProcessedFiles.convention(project.layout.buildDirectory.dir("post-processed-api")) 58 | modelsPackage.convention(tasks.openApiGenerate.flatMap { it.modelPackage }) 59 | } 60 | 61 | sourceSets { 62 | main { 63 | java { 64 | srcDir(postProcessGeneratedApi) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/com/gabrielfeo/examples-test-suite.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | package com.gabrielfeo 4 | 5 | plugins { 6 | id("org.jetbrains.kotlin.jvm") 7 | } 8 | 9 | testing { 10 | suites { 11 | register("examplesTest") { 12 | useKotlinTest() 13 | } 14 | } 15 | } 16 | 17 | kotlin { 18 | target { 19 | val main by compilations.getting 20 | val examplesTest by compilations.getting 21 | examplesTest.associateWith(main) 22 | } 23 | } 24 | 25 | val examples = fileTree(rootDir) { 26 | include("examples/**") 27 | exclude { 28 | it.isDirectory 29 | && (it.name == "build" || it.name.startsWith(".")) 30 | && !it.path.endsWith("buildSrc/src/main/kotlin/build") 31 | } 32 | } 33 | 34 | tasks.named("processExamplesTestResources", ProcessResources::class) { 35 | from(examples) 36 | } 37 | 38 | val downloadPipRequirements by tasks.registering(Exec::class) { 39 | val requirementsFiles = examples.filter { it.name == "requirements.txt" } 40 | inputs.files(requirementsFiles) 41 | .withPropertyName("requirementsFiles") 42 | .withPathSensitivity(PathSensitivity.NONE) 43 | .skipWhenEmpty() 44 | val downloadDir = layout.buildDirectory.dir("pip-requirements") 45 | outputs.dir(downloadDir) 46 | commandLine("pip3", "download") 47 | workingDir(downloadDir) 48 | argumentProviders += CommandLineArgumentProvider { 49 | requirementsFiles.files.flatMap { listOf("-r", it.absolutePath) } 50 | } 51 | } 52 | 53 | tasks.named("examplesTest") { 54 | inputs.files(downloadPipRequirements) 55 | .withPropertyName("downloadedPipRequirements") 56 | .withPathSensitivity(PathSensitivity.NONE) 57 | systemProperty( 58 | "downloaded-requirements-path", 59 | downloadPipRequirements.map { it.outputs.files.singleFile }.get().relativeTo(workingDir).path, 60 | ) 61 | } 62 | 63 | tasks.named("check") { 64 | dependsOn("examplesTest") 65 | } 66 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/com/gabrielfeo/integration-test-suite.gradle.kts: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.jvm") 5 | `java-test-fixtures` 6 | } 7 | 8 | testing { 9 | suites { 10 | register("integrationTest") { 11 | useKotlinTest() 12 | } 13 | } 14 | } 15 | 16 | kotlin { 17 | target { 18 | val main by compilations.getting 19 | val integrationTest by compilations.getting 20 | integrationTest.associateWith(main) 21 | } 22 | } 23 | 24 | tasks.named("check") { 25 | dependsOn("integrationTest") 26 | } 27 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/com/gabrielfeo/kotlin-jvm-library.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | package com.gabrielfeo 4 | 5 | plugins { 6 | id("org.jetbrains.kotlin.jvm") 7 | `java-library` 8 | } 9 | 10 | java { 11 | toolchain { 12 | languageVersion = JavaLanguageVersion.of(17) 13 | vendor = JvmVendorSpec.AZUL 14 | } 15 | consistentResolution { 16 | useRuntimeClasspathVersions() 17 | } 18 | } 19 | 20 | testing { 21 | suites { 22 | named("test") { 23 | useKotlinTest() 24 | } 25 | } 26 | } 27 | 28 | val testTasks = tasks.named { 29 | it == "check" || it.contains("test", ignoreCase = true) 30 | } 31 | 32 | tasks.named { it.startsWith("publish") }.configureEach { 33 | shouldRunAfter(testTasks) 34 | } 35 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/com/gabrielfeo/no-op.gradle.kts: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo 2 | 3 | // Plugin is only used to test the tasks logic. Applying it adds the task classes to the classpath. -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/com/gabrielfeo/published-kotlin-jvm-library.gradle.kts: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo 2 | 3 | import org.jetbrains.dokka.gradle.DokkaExtension 4 | import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier 5 | import java.net.URI 6 | 7 | plugins { 8 | id("com.gabrielfeo.kotlin-jvm-library") 9 | `java-library` 10 | `maven-publish` 11 | signing 12 | id("com.vanniktech.maven.publish.base") 13 | id("org.jetbrains.kotlinx.binary-compatibility-validator") 14 | id("org.jetbrains.dokka") 15 | } 16 | 17 | java { 18 | withSourcesJar() 19 | withJavadocJar() 20 | } 21 | 22 | 23 | configure { 24 | val kotlinSourceRoot = file("src/main/kotlin") 25 | val repoUrlSuffix = "/blob/$version/${kotlinSourceRoot.relativeTo(rootDir)}" 26 | val repoUrl = providers.gradleProperty("repo.url") 27 | .map { URI("$it$repoUrlSuffix").toString() } 28 | dokkaSourceSets.configureEach { 29 | sourceRoots.from(kotlinSourceRoot) 30 | sourceLink { 31 | localDirectory.set(kotlinSourceRoot) 32 | remoteUrl(repoUrl) 33 | remoteLineSuffix = "#L" 34 | } 35 | jdkVersion = java.toolchain.languageVersion.map { it.asInt() } 36 | documentedVisibilities.add(VisibilityModifier.Public) 37 | suppressGeneratedFiles = false 38 | perPackageOption { 39 | matchingRegex = """.*\.internal.*""" 40 | suppress = true 41 | } 42 | listOf( 43 | "https://kotlinlang.org/api/kotlinx.coroutines", 44 | "https://square.github.io/okhttp/5.x/okhttp", 45 | "https://square.github.io/retrofit/2.x/retrofit", 46 | "https://square.github.io/moshi/1.x/moshi", 47 | "https://square.github.io/moshi/1.x/moshi-kotlin", 48 | ).forEach { url -> 49 | val name = url.trim('/').substringAfterLast('/') 50 | externalDocumentationLinks.register(name) { 51 | url(url) 52 | packageListUrl("$url/package-list") 53 | } 54 | } 55 | } 56 | } 57 | 58 | tasks.named("javadocJar") { 59 | from(tasks.dokkaGenerate) 60 | } 61 | 62 | mavenPublishing { 63 | publishToMavenCentral() 64 | } 65 | 66 | fun isCI() = System.getenv("CI").toBoolean() 67 | 68 | signing { 69 | val signedPublications = publishing.publications.matching { 70 | !it.name.contains("unsigned", ignoreCase = true) 71 | } 72 | sign(signedPublications) 73 | if (isCI()) { 74 | useInMemoryPgpKeys( 75 | project.properties["signing.secretKey"] as String?, 76 | project.properties["signing.password"] as String?, 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/com/gabrielfeo/task/PostProcessGeneratedApi.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.task 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.file.DirectoryProperty 5 | import org.gradle.api.file.FileSystemOperations 6 | import org.gradle.api.provider.Property 7 | import org.gradle.api.tasks.* 8 | import org.gradle.kotlin.dsl.withGroovyBuilder 9 | import java.io.File 10 | import javax.inject.Inject 11 | 12 | @CacheableTask 13 | abstract class PostProcessGeneratedApi @Inject constructor( 14 | private val fsOperations: FileSystemOperations, 15 | ) : DefaultTask() { 16 | 17 | @get:InputDirectory 18 | @get:PathSensitive(PathSensitivity.RELATIVE) 19 | abstract val originalFiles: DirectoryProperty 20 | 21 | @get:Input 22 | abstract val modelsPackage: Property 23 | 24 | @get:OutputDirectory 25 | abstract val postProcessedFiles: DirectoryProperty 26 | 27 | @TaskAction 28 | fun doWork() { 29 | postProcessedFiles.get().asFile.deleteRecursively() 30 | fsOperations.copy { 31 | from(originalFiles) 32 | into(postProcessedFiles) 33 | } 34 | postProcess( 35 | srcDir = postProcessedFiles.get().dir("src/main/kotlin").asFile, 36 | modelsPackage = modelsPackage.get() 37 | ) 38 | } 39 | 40 | private fun postProcess(srcDir: File, modelsPackage: String) { 41 | // Add @JvmSuppressWildcards to avoid square/retrofit#3275 42 | replaceAll( 43 | match = "interface", 44 | replace = "@JvmSuppressWildcards\ninterface", 45 | dir = srcDir, 46 | includes = "com/gabrielfeo/develocity/api/*Api.kt", 47 | ) 48 | // Fix mapping of BuildModelName: gradle-attributes -> gradleAttributes 49 | replaceAll( 50 | match = "Minus", 51 | replace = "", 52 | dir = srcDir, 53 | includes = "com/gabrielfeo/develocity/api/model/BuildModelName.kt", 54 | ) 55 | // Fix mapping of GradleConfigurationCacheResult.Outcome: hIT -> hit 56 | val file = "com/gabrielfeo/develocity/api/model/GradleConfigurationCacheResult.kt" 57 | replaceAll("hIT", "hit", dir = srcDir, includes = file) 58 | replaceAll("mISS", "miss", dir = srcDir, includes = file) 59 | replaceAll("fAILED", "failed", dir = srcDir, includes = file) 60 | 61 | // Fix mapping of MavenExtension.Type: lIBEXT -> core 62 | val mavenExtensionFile = "com/gabrielfeo/develocity/api/model/MavenExtension.kt" 63 | mapOf( 64 | "cORE" to "core", 65 | "mAVENEXTCLASSPATH" to "mavenExtClasspath", 66 | "lIBEXT" to "libExt", 67 | "pROJECT" to "project", 68 | "pOM" to "pom", 69 | "uNKNOWN" to "unknown", 70 | ).forEach { (match, replace) -> 71 | replaceAll(match, replace, dir = srcDir, includes = mavenExtensionFile) 72 | } 73 | } 74 | 75 | private fun replaceAll( 76 | match: String, 77 | replace: String, 78 | dir: File, 79 | includes: String, 80 | ) { 81 | ant.withGroovyBuilder { 82 | "replaceregexp"( 83 | "match" to match, 84 | "replace" to replace, 85 | "flags" to "mg", 86 | ) { 87 | "fileset"( 88 | "dir" to dir, 89 | "includes" to includes, 90 | ) 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/com/gabrielfeo/test-fixtures.gradle.kts: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.jvm") 5 | `java-test-fixtures` 6 | } 7 | 8 | kotlin { 9 | target { 10 | val main by compilations.getting 11 | val testFixtures by compilations.getting 12 | testFixtures.associateWith(main) 13 | compilations.named { it.endsWith("test", ignoreCase = true) }.configureEach { 14 | associateWith(testFixtures) 15 | } 16 | } 17 | } 18 | 19 | components.named("java") { 20 | withVariantsFromConfiguration(configurations["testFixturesApiElements"]) { skip() } 21 | withVariantsFromConfiguration(configurations["testFixturesRuntimeElements"]) { skip() } 22 | } 23 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.gabrielfeo.kotlin-jvm-library") apply false 3 | } 4 | 5 | tasks.register("check") { 6 | dependsOn(gradle.includedBuilds.map { it.task(":check") }) 7 | } 8 | -------------------------------------------------------------------------------- /docs/AccessKeys.md: -------------------------------------------------------------------------------- 1 | # Access keys 2 | 3 | API requests may require [authentication][1]. 4 | 5 | ## Anonymous access 6 | 7 | If your Develocity server is configured to allow [anonymous access][2] for permission "Access build data via the API", then an access key is not required to use this library. 8 | This is often the case for a server only accessible from a private network. 9 | 10 | ![Anonymous access settings](media/AnonymousAccessPage.png) 11 | 12 | ## Authenticated access 13 | 14 | The library will automatically resolve the access key using the same conventions as official Develocity tooling, in order: 15 | 16 | - Environment variable `DEVELOCITY_ACCESS_KEY` 17 | - Environment variable `GRADLE_ENTERPRISE_ACCESS_KEY` 18 | - File `$GRADLE_USER_HOME/.gradle/develocity/keys.properties` (or `~/.gradle/develocity/keys.properties` if `GRADLE_USER_HOME` is not set) 19 | - File `~/.m2/.develocity/keys.properties` 20 | 21 | Please check if you already have an access key set up in your build environment for the Develocity server you want to query. The first key for a matching host will be used, if found. 22 | 23 | See the official manuals for instructions on how to set up a new access key in one of these locations: 24 | 25 | - [Develocity Gradle Plugin User Manual][3] 26 | - [Develocity Maven Extension User Manual][4] 27 | - [Develocity sbt Plugin User Manual][5] 28 | - [Develocity npm Agent User Manual][6] 29 | - [Develocity Python Agent User Manual][7] 30 | 31 | ### User permissions 32 | 33 | To call the API, the user from which the access key was generated must have the "Access build data via the API" permission. 34 | 35 | ![User permissions](media/AccessPage.png) 36 | 37 | [1]: https://docs.gradle.com/enterprise/api-manual/#access_control 38 | [2]: https://docs.gradle.com/develocity/helm-admin/current/#_anonymous_access 39 | [3]: https://docs.gradle.com/develocity/gradle-plugin/current/#manual_access_key_configuration 40 | [4]: https://docs.gradle.com/develocity/maven-extension/current/#manual_access_key_configuration 41 | [5]: https://gradle.com/help/sbt-plugin-authenticating 42 | [6]: https://gradle.com/help/npm-agent-authenticating 43 | [7]: https://gradle.com/help/python-agent-authenticating 44 | -------------------------------------------------------------------------------- /docs/Logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | This library uses [SLF4J][1] but does not bundle an SLF4J implementation. 4 | 5 | - [Notebooks](#notebooks) 6 | - [Scripts](#scripts) 7 | - [`simple-logger`](#simple-logger) 8 | - [Projects](#projects) 9 | - [`simple-logger`](#simple-logger-1) 10 | 11 | ## Notebooks 12 | 13 | 1. Set `%logLevel ` in a code cell, e.g., `%logLevel debug`. 14 | 15 | Logs appear in the Kotlin Jupyter kernel logs: 16 | 17 | - In Jupyter and JupyterLab, logs appear in the shell that owns the Jupyter process 18 | - In IntelliJ, view logs in the Kotlin Notebook logs tool window 19 | 20 | ![IntelliJ Kotlin Notebook logs tool window](media/IntelliJKernelLogs.png) 21 | 22 | ⚠️ Older versions of the Kotlin Jupyter kernel had issues with logging. Kernel version `0.15.0-598` and higher are known to work. In IntelliJ, configure to use a later version than bundled. In pip and conda, update the kernel package. 23 | 24 | ![IntelliJ Kotlin Jupyter kernel version configuration](media/IntelliJKernelSettings.png) 25 | 26 | See the example notebook [Logging.ipynb](../examples/example-notebooks/Logging.ipynb). 27 | 28 | ## Scripts 29 | 30 | 1. Add an SLF4J implementation (e.g., `slf4j-simple`, `logback-classic`, etc.) 31 | 2. Set the log level for the package `com.gabrielfeo.develocity.api` using your chosen logging framework's configuration 32 | 33 | ### `simple-logger` 34 | 35 | Adding `simple-logger` to your classpath is the easiest way to get logging in scripts. You can do this by adding the following line to your script: 36 | 37 | ```kotlin 38 | @file:DependsOn("org.slf4j:slf4j-simple:2.0.17") 39 | ``` 40 | 41 | Then set the log level for `com.gabrielfeo.develocity.api` using system properties. For example: 42 | 43 | - from script code 44 | 45 | ```kotlin 46 | @file:DependsOn("com.gabrielfeo:develocity-api-kotlin:2025.1.1") 47 | @file:DependsOn("org.slf4j:slf4j-simple:2.0.17") 48 | 49 | System.setProperty("org.slf4j.simpleLogger.log.com.gabrielfeo.develocity.api", "debug") 50 | 51 | // ... 52 | ``` 53 | 54 | - from the shebang line 55 | 56 | ```kotlin 57 | #!/usr/bin/env kotlin -script -J-Dorg.slf4j.simpleLogger.log.com.gabrielfeo.develocity.api=debug 58 | 59 | @file:DependsOn("com.gabrielfeo:develocity-api-kotlin:2025.1.1") 60 | @file:DependsOn("org.slf4j:slf4j-simple:2.0.17") 61 | 62 | // ... 63 | ``` 64 | 65 | - from `JAVA_OPTS` 66 | 67 | ```bash 68 | export JAVA_OPTS="-Dorg.slf4j.simpleLogger.log.com.gabrielfeo.develocity.api=debug" 69 | kotlin -script example-script.main.kts 70 | ``` 71 | 72 | ## Projects 73 | 74 | 1. Add an SLF4J implementation (e.g., `slf4j-simple`, `logback-classic`, etc.) to your classpath 75 | 2. Set the log level for the package `com.gabrielfeo.develocity.api` using your chosen logging framework's configuration 76 | 77 | ### `simple-logger` 78 | 79 | Adding `simple-logger` to your classpath is the easiest way to get logging in projects. You can do this by adding the following dependency to your build file: 80 | 81 | ```kotlin 82 | // build.gradle.kts 83 | dependencies { 84 | implementation("com.gabrielfeo:develocity-api-kotlin:2025.1.1") 85 | runtimeOnly("org.slf4j:slf4j-simple:2.0.17") 86 | } 87 | ``` 88 | 89 | Then set the system property when running your application. If using the Gradle `run` task, it can be declared in your build: 90 | 91 | ```kotlin 92 | tasks.named("run") { 93 | // ... 94 | systemProperty("org.slf4j.simpleLogger.log.com.gabrielfeo.develocity.api", "debug") 95 | } 96 | ``` 97 | 98 | [0]: https://www.slf4j.org/ 99 | -------------------------------------------------------------------------------- /docs/media/AccessPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielfeo/develocity-api-kotlin/c50cd98e0f9ac69f1b1386c92ea92c1b6537614a/docs/media/AccessPage.png -------------------------------------------------------------------------------- /docs/media/AnonymousAccessPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielfeo/develocity-api-kotlin/c50cd98e0f9ac69f1b1386c92ea92c1b6537614a/docs/media/AnonymousAccessPage.png -------------------------------------------------------------------------------- /docs/media/IntelliJKernelLogs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielfeo/develocity-api-kotlin/c50cd98e0f9ac69f1b1386c92ea92c1b6537614a/docs/media/IntelliJKernelLogs.png -------------------------------------------------------------------------------- /docs/media/IntelliJKernelSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielfeo/develocity-api-kotlin/c50cd98e0f9ac69f1b1386c92ea92c1b6537614a/docs/media/IntelliJKernelSettings.png -------------------------------------------------------------------------------- /examples/example-gradle-task/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Example usage in build logic 4 | 5 | This example shows how to create a reusable Gradle plugin that adds a `userBuildPerformanceMetrics` task to fetch and print build performance metrics for a specific user, using the Develocity API Kotlin client. 6 | 7 | ## Core files 8 | 9 | - [`build-logic/src/main/kotlin/build/logic/PerformanceMetricsTask.kt`](./build-logic/src/main/kotlin/build/logic/PerformanceMetricsTask.kt): Implements a custom Gradle task that fetches and prints build performance metrics of a given user. 10 | - [`build-logic/src/main/kotlin/build/logic/DevelocityApiService.kt`](./build-logic/src/main/kotlin/build/logic/DevelocityApiService.kt): Defines a shared build service containing the Develocity API client, which could be used by multiple tasks while ensuring a singleton client per build. 11 | - [`build-logic/performance-metrics-plugin.gradle.kts`](./build-logic/performance-metrics-plugin.gradle.kts): A plugin which registers the task where it's applied. 12 | - [`build.gradle.kts`](./build.gradle.kts): Applies `performance-metrics-plugin`, making the task available for this build. 13 | 14 | ## Usage 15 | 16 | Run: 17 | 18 | ```sh 19 | ./gradlew userBuildPerformanceMetrics [--user=foo] --period=[-1d|-7d|-14d|-30d|...] 20 | ``` 21 | -------------------------------------------------------------------------------- /examples/example-gradle-task/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("build.logic.performance-metrics-plugin") 3 | } 4 | -------------------------------------------------------------------------------- /examples/example-gradle-task/buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | dependencies { 6 | implementation("com.gabrielfeo:develocity-api-kotlin:2025.1.1") 7 | implementation("org.apache.commons:commons-math3:3.6.1") 8 | } 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | -------------------------------------------------------------------------------- /examples/example-gradle-task/buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "dak-example-gradle-task-build-src" 2 | -------------------------------------------------------------------------------- /examples/example-gradle-task/buildSrc/src/main/kotlin/build/logic/DevelocityApiService.kt: -------------------------------------------------------------------------------- 1 | package build.logic 2 | 3 | import com.gabrielfeo.develocity.api.Config 4 | import com.gabrielfeo.develocity.api.DevelocityApi 5 | import org.gradle.api.services.BuildService 6 | import org.gradle.api.services.BuildServiceParameters 7 | import okhttp3.OkHttpClient 8 | 9 | abstract class DevelocityApiService 10 | : DevelocityApi by DevelocityApi.newInstance(), 11 | BuildService, 12 | AutoCloseable { 13 | 14 | override fun close() { 15 | shutdown() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/example-gradle-task/buildSrc/src/main/kotlin/build/logic/PerformanceMetricsTask.kt: -------------------------------------------------------------------------------- 1 | package build.logic 2 | 3 | import com.gabrielfeo.develocity.api.DevelocityApi 4 | import kotlinx.coroutines.runBlocking 5 | import com.gabrielfeo.develocity.api.model.BuildModelName 6 | import com.gabrielfeo.develocity.api.model.Build 7 | import com.gabrielfeo.develocity.api.model.GradleBuildCachePerformance 8 | import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics 9 | import org.gradle.api.DefaultTask 10 | import org.gradle.api.provider.Property 11 | import org.gradle.api.tasks.Input 12 | import org.gradle.api.tasks.Optional 13 | import org.gradle.api.tasks.TaskAction 14 | import org.gradle.api.tasks.options.Option 15 | import org.gradle.api.services.ServiceReference 16 | import java.time.Duration 17 | 18 | 19 | abstract class PerformanceMetricsTask( 20 | 21 | ) : DefaultTask() { 22 | 23 | @get:Optional 24 | @get:Input 25 | @get:Option( 26 | option = "user", 27 | description = "The user to query builds for. Defaults to the current OS username." 28 | ) 29 | abstract val user: Property 30 | 31 | @get:Optional 32 | @get:Input 33 | @get:Option( 34 | option = "period", 35 | description = "The period to query builds for (e.g. -14d, -30d, etc). Default: -14d." 36 | ) 37 | abstract val period: Property 38 | 39 | @get:ServiceReference 40 | abstract val api: Property 41 | 42 | @TaskAction 43 | fun run() { 44 | val user = user.getOrElse(System.getProperty("user.name")) 45 | val startTime = period.getOrElse("-14d") 46 | val metrics = runBlocking { 47 | getUserBuildsPerformanceMetrics(api.get(), user, startTime) 48 | } 49 | logger.quiet(metrics) 50 | } 51 | 52 | suspend fun getUserBuildsPerformanceMetrics( 53 | api: DevelocityApi, 54 | user: String, 55 | startTime: String, 56 | ): String { 57 | val query = """user:"$user" buildStartTime>$startTime""" 58 | val buildsPerformanceData = fetchBuildsPerformanceData(api, query) 59 | val serializationFactors = buildsPerformanceData 60 | .map { it.serializationFactor } 61 | .let { DescriptiveStatistics(it.toDoubleArray()) } 62 | val avoidanceSavings = buildsPerformanceData 63 | .map { it.workUnitAvoidanceSavingsSummary.ratio } 64 | .let { DescriptiveStatistics(it.toDoubleArray()) } 65 | val heading = "Build performance overview for $user since $startTime (powered by Develocity®)" 66 | return """ 67 | | 68 | |${"\u001B[1;36m"}$heading${"\u001B[0m"} 69 | | ▶︎ Serialization factor: %.1fx 70 | | (Gradle's parallel execution) 71 | | ⏩︎ Avoidance savings: %.1f%% (mean) ~ %.1f%% (p95) 72 | | (Gradle and Develocity's mechanisms, incl. incremental build and remote cache) 73 | """.trimMargin().format( 74 | serializationFactors.mean, 75 | avoidanceSavings.mean, 76 | avoidanceSavings.getPercentile(95.0), 77 | ) 78 | } 79 | 80 | private suspend fun fetchBuildsPerformanceData( 81 | api: DevelocityApi, 82 | query: String, 83 | ): List { 84 | return api.buildsApi.getBuilds( 85 | fromInstant = 0, 86 | query = query, 87 | models = listOf(BuildModelName.gradleBuildCachePerformance), 88 | ).mapNotNull { build -> 89 | build.models?.gradleBuildCachePerformance?.model 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /examples/example-gradle-task/buildSrc/src/main/kotlin/build/logic/performance-metrics-plugin.gradle.kts: -------------------------------------------------------------------------------- 1 | package build.logic 2 | 3 | gradle.sharedServices.registerIfAbsent("develocityApiService", DevelocityApiService::class) 4 | 5 | tasks.register("userBuildPerformanceMetrics") { 6 | group = "Develocity" 7 | description = "Retrieves performance metrics for the user's builds from Develocity API." 8 | } 9 | -------------------------------------------------------------------------------- /examples/example-gradle-task/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielfeo/develocity-api-kotlin/c50cd98e0f9ac69f1b1386c92ea92c1b6537614a/examples/example-gradle-task/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/example-gradle-task/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /examples/example-gradle-task/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /examples/example-gradle-task/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "dak-example-gradle-task-main-build" 2 | -------------------------------------------------------------------------------- /examples/example-notebooks/Logging.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "metadata": {}, 5 | "cell_type": "code", 6 | "outputs": [], 7 | "execution_count": null, 8 | "source": [ 9 | "%useLatestDescriptors\n", 10 | "%use develocity-api-kotlin(version=2025.1.1)\n", 11 | "%use coroutines(v=1.7.1)\n", 12 | "\n", 13 | "val api = DevelocityApi.newInstance(config = Config(cacheConfig = Config.CacheConfig(cacheEnabled = false)))" 14 | ] 15 | }, 16 | { 17 | "metadata": {}, 18 | "cell_type": "code", 19 | "outputs": [], 20 | "execution_count": null, 21 | "source": [ 22 | "%logLevel debug\n", 23 | "\n", 24 | "runBlocking {\n", 25 | " api.buildsApi.getBuildsFlow(\n", 26 | " fromInstant = 0,\n", 27 | " query = \"\"\"buildStartTime>-7d buildTool:gradle\"\"\",\n", 28 | " ).last()\n", 29 | "}" 30 | ] 31 | }, 32 | { 33 | "metadata": {}, 34 | "cell_type": "markdown", 35 | "source": [ 36 | "Expect logs such as these in kernel logs. See [docs/Logging.md][1] for more details.\n", 37 | "\n", 38 | "##### Cache hits\n", 39 | "\n", 40 | "```\n", 41 | "3764 [Execution of code '%logLevel debug...'] DEBUG c.gabrielfeo.develocity.api.Cache - HTTP cache dir: /Users/gfeo/.develocity-api-kotlin-cache (max 1000000000B)\n", 42 | "4053 [OkHttp https://ge.solutions-team.gradle.com/...] DEBUG c.gabrielfeo.develocity.api.Cache - Cache hit: https://ge.solutions-team.gradle.com/api/builds?fromInstant=0&maxBuilds=1000&query=buildStartTime%3E-7d%20buildTool%3Agradle&allModels=false\n", 43 | "4072 [OkHttp https://ge.solutions-team.gradle.com/...] DEBUG c.gabrielfeo.develocity.api.Cache - Cache hit: https://ge.solutions-team.gradle.com/api/builds?fromBuild=qcku2w347d5dy&maxBuilds=1000&query=buildStartTime%3E-7d%20buildTool%3Agradle&allModels=false\n", 44 | "4072 [OkHttp https://ge.solutions-team.gradle.com/...] DEBUG c.gabrielfeo.develocity.api.OkHttpClient - Cache hit: https://ge.solutions-team.gradle.com/api/builds?fromBuild=qcku2w347d5dy&maxBuilds=1000&query=buildStartTime%3E-7d%20buildTool%3Agradle&allModels=false\n", 45 | "```\n", 46 | "\n", 47 | "##### Cache misses\n", 48 | "\n", 49 | "```\n", 50 | "3447 [Execution of code '%logLevel debug...'] DEBUG c.gabrielfeo.develocity.api.Cache - HTTP cache is disabled\n", 51 | "3853 [OkHttp https://ge.solutions-team.gradle.com/...] DEBUG c.g.develocity.api.OkHttpClient - --> GET https://ge.solutions-team.gradle.com/api/builds?fromInstant=0&maxBuilds=1000&query=buildStartTime%3E-7d%20buildTool%3Agradle&allModels=false h2\n", 52 | "4208 [OkHttp https://ge.solutions-team.gradle.com/...] DEBUG c.g.develocity.api.OkHttpClient - <-- 200 https://ge.solutions-team.gradle.com/api/builds?fromInstant=0&maxBuilds=1000&query=buildStartTime%3E-7d%20buildTool%3Agradle&allModels=false (355ms, unknown-length body)\n", 53 | "4230 [OkHttp https://ge.solutions-team.gradle.com/...] DEBUG c.g.develocity.api.OkHttpClient - --> GET https://ge.solutions-team.gradle.com/api/builds?fromBuild=qcku2w347d5dy&maxBuilds=1000&query=buildStartTime%3E-7d%20buildTool%3Agradle&allModels=false h2\n", 54 | "4424 [OkHttp https://ge.solutions-team.gradle.com/...] DEBUG c.g.develocity.api.OkHttpClient - <-- 200 https://ge.solutions-team.gradle.com/api/builds?fromBuild=qcku2w347d5dy&maxBuilds=1000&query=buildStartTime%3E-7d%20buildTool%3Agradle&allModels=false (193ms, unknown-length body)\n", 55 | "```\n", 56 | "\n", 57 | "[1]: https://github.com/gabrielfeo/develocity-api-kotlin/blob/main/docs/Logging.md" 58 | ] 59 | } 60 | ], 61 | "metadata": { 62 | "kernelspec": { 63 | "display_name": "Kotlin", 64 | "language": "kotlin", 65 | "name": "kotlin" 66 | }, 67 | "language_info": { 68 | "codemirror_mode": "text/x-kotlin", 69 | "file_extension": ".kt", 70 | "mimetype": "text/x-kotlin", 71 | "name": "kotlin", 72 | "nbconvert_exporter": "", 73 | "pygments_lexer": "kotlin", 74 | "version": "2.2.20-Beta2" 75 | } 76 | }, 77 | "nbformat": 4, 78 | "nbformat_minor": 0 79 | } 80 | -------------------------------------------------------------------------------- /examples/example-notebooks/requirements.txt: -------------------------------------------------------------------------------- 1 | jupyterlab==4.4.8 2 | kotlin-jupyter-kernel==0.15.0.598 3 | -------------------------------------------------------------------------------- /examples/example-project/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "2.2.10" 3 | application 4 | } 5 | 6 | application { 7 | mainClass = "com.gabrielfeo.develocity.api.example.MainKt" 8 | } 9 | 10 | dependencies { 11 | implementation("com.gabrielfeo:develocity-api-kotlin:2025.1.1") 12 | } 13 | 14 | repositories { 15 | mavenCentral() 16 | } 17 | -------------------------------------------------------------------------------- /examples/example-project/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielfeo/develocity-api-kotlin/c50cd98e0f9ac69f1b1386c92ea92c1b6537614a/examples/example-project/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/example-project/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /examples/example-project/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /examples/example-project/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "dak-example-gradle-project" 2 | -------------------------------------------------------------------------------- /examples/example-project/src/main/kotlin/com/gabrielfeo/develocity/api/example/Main.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example 2 | 3 | import com.gabrielfeo.develocity.api.Config 4 | import com.gabrielfeo.develocity.api.DevelocityApi 5 | import com.gabrielfeo.develocity.api.example.analysis.mostFrequentBuilds 6 | import okhttp3.OkHttpClient 7 | 8 | /* 9 | * Example main that runs all API analysis at once. In projects, you can share an 10 | * OkHttpClient.Builder between DevelocityApi and your own project classes, in order to 11 | * save resources. 12 | */ 13 | 14 | val clientBuilder = OkHttpClient.Builder() 15 | 16 | suspend fun main(args: Array) { 17 | val newConfig = Config( 18 | clientBuilder = clientBuilder, 19 | ) 20 | val develocityApi = DevelocityApi.newInstance(newConfig) 21 | val query = args.getOrElse(0) { "buildStartTime>-1d buildTool:gradle" } 22 | runAllAnalysis(develocityApi, query) 23 | develocityApi.shutdown() 24 | } 25 | 26 | private suspend fun runAllAnalysis(develocityApi: DevelocityApi, query: String) { 27 | mostFrequentBuilds(api = develocityApi.buildsApi, query) 28 | } 29 | -------------------------------------------------------------------------------- /examples/example-project/src/main/kotlin/com/gabrielfeo/develocity/api/example/analysis/MostFrequentBuilds.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example.analysis 2 | 3 | import com.gabrielfeo.develocity.api.* 4 | import com.gabrielfeo.develocity.api.extension.* 5 | import com.gabrielfeo.develocity.api.model.* 6 | import kotlinx.coroutines.flow.* 7 | import java.util.LinkedList 8 | 9 | /** 10 | * See what builds are most commonly invoked by developers, e.g. 'clean assemble', 11 | * 'test' or 'check'. You can set up the URL and a token for your Gradle 12 | * Enterprise instance and run this notebook as-is for your own project. This is a 13 | * simple example of something you can do with the API. It could bring insights, 14 | * for example: 15 | * 16 | * - "Our developers frequently clean together with assemble. We should ask them why, 17 | * because they shouldn't have to. Just an old habit from Maven or are they working 18 | * around a build issue we don't know about?" 19 | * 20 | * - "Some are doing check builds locally, which we set up to trigger our notably slow 21 | * legacy tests. We should suggest they run test instead, leaving check for CI to run." 22 | */ 23 | suspend fun mostFrequentBuilds( 24 | api: BuildsApi, 25 | query: String, 26 | ) { 27 | // Fetch builds from the API 28 | val builds: List = api.getBuildsFlow( 29 | fromInstant = 0, 30 | query = query, 31 | models = listOf(BuildModelName.gradleAttributes), 32 | ).map { 33 | it.models!!.gradleAttributes!!.model!! 34 | }.toList(LinkedList()) 35 | 36 | // Process builds and count how many times each was invoked 37 | check(builds.isNotEmpty()) { "No builds found. Adjust query and try again." } 38 | val buildCounts = builds.groupBy { build -> 39 | val tasks = build.requestedTasks.joinToString(" ").trim(':') 40 | tasks.ifBlank { "IDE sync" } 41 | }.mapValues { (_, builds) -> 42 | builds.size 43 | }.entries.sortedByDescending { (_, count) -> 44 | count 45 | } 46 | 47 | // Print the top 5 as a pretty table 48 | val table = buildCounts.take(5).joinToString("\n") { (tasks, count) -> 49 | "${tasks.padEnd(100)} | $count" 50 | } 51 | println( 52 | """ 53 | |--------------------- 54 | |Most frequent builds: 55 | | 56 | |$table 57 | """.trimMargin() 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /examples/example-scripts/example-script.main.kts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kotlinc -script 2 | 3 | /* 4 | * See what builds are most commonly invoked by developers, e.g. 'clean assemble', 5 | * 'test' or 'check'. You can set up the URL and a token for your Gradle 6 | * Enterprise instance and run this notebook as-is for your own project. This is a 7 | * simple example of something you can do with the API. It could bring insights, 8 | * for example: 9 | * 10 | * - "Our developers frequently clean together with assemble. We should ask them why, 11 | * because they shouldn't have to. Just an old habit from Maven or are they working 12 | * around a build issue we don't know about?" 13 | * 14 | * - "Some are doing check builds locally, which we set up to trigger our notably slow 15 | * legacy tests. We should suggest they run test instead, leaving check for CI to run." 16 | * 17 | * Run this with at least 1GB of heap to accommodate the fetched data: JAVA_OPTS=-Xmx1g 18 | */ 19 | 20 | @file:DependsOn("com.gabrielfeo:develocity-api-kotlin:2025.1.1") 21 | 22 | import com.gabrielfeo.develocity.api.* 23 | import com.gabrielfeo.develocity.api.model.* 24 | import com.gabrielfeo.develocity.api.extension.* 25 | import kotlinx.coroutines.* 26 | import kotlinx.coroutines.flow.* 27 | import java.util.LinkedList 28 | 29 | // Parameters 30 | val query = args.getOrElse(0) { "buildStartTime>-7d buildTool:gradle" } 31 | 32 | // Fetch builds from the API 33 | val api = DevelocityApi.newInstance() 34 | val builds: List = runBlocking { 35 | api.buildsApi.getBuildsFlow( 36 | fromInstant = 0, 37 | query = query, 38 | models = listOf(BuildModelName.gradleAttributes), 39 | ).map { 40 | it.models!!.gradleAttributes!!.model!! 41 | }.toList(LinkedList()) 42 | } 43 | 44 | // Process builds and count how many times each was invoked 45 | check(builds.isNotEmpty()) { "No builds found. Adjust query and try again." } 46 | val buildCounts = builds.groupBy { build -> 47 | val tasks = build.requestedTasks.joinToString(" ").trim(':') 48 | tasks.ifBlank { "IDE sync" } 49 | }.mapValues { (_, builds) -> 50 | builds.size 51 | }.entries.sortedByDescending { (_, count) -> 52 | count 53 | } 54 | 55 | // Print the top 5 as a pretty table 56 | val table = buildCounts.take(5).joinToString("\n") { (tasks, count) -> 57 | "${tasks.padEnd(100)} | $count" 58 | } 59 | println( 60 | """ 61 | |--------------------- 62 | |Most frequent builds: 63 | | 64 | |$table 65 | """.trimMargin() 66 | ) 67 | 68 | // Shutdown to end background threads and allow script to exit earlier (see README) 69 | api.shutdown() 70 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=com.gabrielfeo 2 | artifact=develocity-api-kotlin 3 | version=2025.2.0 4 | develocity.version=2025.2 5 | repo.url=https://github.com/gabrielfeo/develocity-api-kotlin 6 | org.gradle.parallel=true 7 | org.gradle.jvmargs=-Xmx5g 8 | org.gradle.caching=true 9 | org.gradle.configuration-cache=true 10 | org.gradle.configuration-cache.parallel=true 11 | # Becomes default in Gradle 9.0 12 | org.gradle.kotlin.dsl.skipMetadataVersionCheck=false 13 | # Explicitly added at different versions, due to Kotlin/kotlin-jupyter#462 14 | kotlin.jupyter.add.api=false 15 | kotlin.jupyter.add.scanner=false 16 | kotlin.jupyter.add.testkit=false 17 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 18 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 19 | -------------------------------------------------------------------------------- /gradle/gradle-daemon-jvm.properties: -------------------------------------------------------------------------------- 1 | #This file is generated by updateDaemonJvm 2 | toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6fb1b1f1c4016d56e240f38e31b0615f/redirect 3 | toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/bebd8b980a4cdad3dd6f9d1c3f194c08/redirect 4 | toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6fb1b1f1c4016d56e240f38e31b0615f/redirect 5 | toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/bebd8b980a4cdad3dd6f9d1c3f194c08/redirect 6 | toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/35ea6c5d874634e79bd0dcf2704df0e8/redirect 7 | toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/bdb01d23d4d09037d47c81bbe1b947e1/redirect 8 | toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6fb1b1f1c4016d56e240f38e31b0615f/redirect 9 | toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/bebd8b980a4cdad3dd6f9d1c3f194c08/redirect 10 | toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/fa5701aaf7265fd19b6f642c567a4fbb/redirect 11 | toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/74794a7f60a73cdd790f439c37c7a608/redirect 12 | toolchainVendor=BELLSOFT 13 | toolchainVersion=24 14 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.2.10" 3 | kotlin-stdlib = "2.0.0" 4 | dokka = "2.0.0" 5 | openapi-generator = "7.16.0" 6 | jupyter = "0.15.0-650" 7 | okio = "3.16.1" 8 | moshi = "1.15.2" 9 | okhttp = "4.12.0" 10 | retrofit = "3.0.0" 11 | kotlin-coroutines = "1.10.2" 12 | kotlin-binary-compatibility-validator = "0.18.1" 13 | slf4j = "2.0.17" 14 | logback = "1.5.19" 15 | guava = "33.5.0-jre" 16 | maven-publish-plugin = "0.34.0" 17 | 18 | [libraries] 19 | junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params" } 20 | okio = { module = "com.squareup.okio:okio", version.ref = "okio" } 21 | okio-fakeFileSystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } 22 | moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } 23 | moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } 24 | okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } 25 | okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } 26 | okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } 27 | retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } 28 | retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } 29 | retrofit-converter-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "retrofit" } 30 | kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } 31 | kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlin-coroutines" } 32 | kotlin-jupyter-api = { module = "org.jetbrains.kotlinx:kotlin-jupyter-api", version.ref = "jupyter" } 33 | kotlin-jupyter-testkit = { module = "org.jetbrains.kotlinx:kotlin-jupyter-test-kit", version.ref = "jupyter" } 34 | kotlin-binary-compatibility-validator-plugin = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "kotlin-binary-compatibility-validator" } 35 | kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 36 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin-stdlib" } 37 | dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } 38 | openapi-generator-plugin = { module = "org.openapitools:openapi-generator-gradle-plugin", version.ref = "openapi-generator" } 39 | slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } 40 | logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } 41 | logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" } 42 | guava = { module = "com.google.guava:guava", version.ref = "guava" } 43 | vanniktech-mavenPublishPlugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish-plugin" } 44 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielfeo/develocity-api-kotlin/c50cd98e0f9ac69f1b1386c92ea92c1b6537614a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /library/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | build/generated-api/* 2 | build/generated-api/.* 3 | build/generated-api/.github/* 4 | build/generated-api/api/* 5 | build/generated-api/gradle/**/* 6 | build/generated-api/docs/* 7 | build/generated-api/src/main/AndroidManifest.xml 8 | build/generated-api/src/main/kotlin/com/gabrielfeo/develocity/api/internal/infrastructure/ApiClient.kt 9 | build/generated-api/src/main/kotlin/com/gabrielfeo/develocity/api/GradleEnterpriseApi.kt 10 | build/generated-api/src/main/kotlin/com/gabrielfeo/develocity/api/DevelocityApi.kt 11 | build/generated-api/src/test/**/* 12 | -------------------------------------------------------------------------------- /library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 3 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 4 | 5 | plugins { 6 | id("com.gabrielfeo.published-kotlin-jvm-library") 7 | id("com.gabrielfeo.develocity-api-code-generation") 8 | id("com.gabrielfeo.integration-test-suite") 9 | id("com.gabrielfeo.examples-test-suite") 10 | id("com.gabrielfeo.test-fixtures") 11 | } 12 | 13 | // Order matters as this library is used as a Kotlin Jupyter kernel dependency (see #440) 14 | dependencies { 15 | constraints { 16 | implementation(libs.okio) 17 | } 18 | // Set fixed version of stdlib for compatibility with the version of Kotlin 19 | // embedded in earlier versions of Gradle 20 | implementation(libs.kotlin.stdlib) 21 | api(libs.okhttp) 22 | implementation(libs.okhttp.logging.interceptor) 23 | api(libs.retrofit) 24 | implementation(libs.retrofit.converter.moshi) 25 | implementation(libs.retrofit.converter.scalars) 26 | api(libs.moshi) 27 | implementation(libs.moshi.kotlin) 28 | api(libs.kotlin.coroutines) 29 | implementation(libs.slf4j.api) 30 | compileOnly(libs.kotlin.jupyter.api) 31 | testImplementation(libs.okhttp.mockwebserver) 32 | testImplementation(libs.okio.fakeFileSystem) 33 | testImplementation(libs.okio) 34 | testImplementation(libs.kotlin.coroutines.test) 35 | testImplementation(libs.junit.jupiter.params) 36 | integrationTestImplementation(libs.kotlin.coroutines.test) 37 | integrationTestImplementation(libs.guava) 38 | integrationTestImplementation(libs.kotlin.jupyter.testkit) 39 | integrationTestImplementation(libs.logback.core) 40 | integrationTestImplementation(libs.logback.classic) 41 | integrationTestImplementation(libs.okhttp.mockwebserver) 42 | } 43 | 44 | val libraryPom = Action { 45 | name = "Develocity API Kotlin" 46 | description = "A library to use the Develocity API in Kotlin" 47 | val repoUrl = providers.gradleProperty("repo.url") 48 | url = repoUrl 49 | licenses { 50 | license { 51 | name = "MIT" 52 | url = "https://spdx.org/licenses/MIT.html" 53 | distribution = "repo" 54 | } 55 | } 56 | developers { 57 | developer { 58 | id = "gabrielfeo" 59 | name = "Gabriel Feo" 60 | email = "gabriel@gabrielfeo.com" 61 | } 62 | } 63 | scm { 64 | val basicUrl = repoUrl.map { it.substringAfter("://") } 65 | connection = basicUrl.map { "scm:git:git://$it.git" } 66 | developerConnection = basicUrl.map { "scm:git:ssh://$it.git" } 67 | url = basicUrl.map { "https://$it/" } 68 | } 69 | } 70 | 71 | publishing { 72 | publications { 73 | register("develocityApiKotlin") { 74 | artifactId = "develocity-api-kotlin" 75 | from(components["java"]) 76 | pom(libraryPom) 77 | } 78 | // For occasional maven local publishing 79 | register("unsignedDevelocityApiKotlin") { 80 | artifactId = "develocity-api-kotlin" 81 | from(components["java"]) 82 | pom(libraryPom) 83 | } 84 | register("unsignedSnapshotDevelocityApiKotlin") { 85 | artifactId = "develocity-api-kotlin" 86 | version = "SNAPSHOT" 87 | from(components["java"]) 88 | pom(libraryPom) 89 | } 90 | register("relocation") { 91 | artifactId = "gradle-enterprise-api-kotlin" 92 | pom { 93 | libraryPom(this) 94 | distributionManagement { 95 | relocation { 96 | groupId = project.group.toString() 97 | artifactId = "develocity-api-kotlin" 98 | message = "artifactId has been changed. Part of the rename to Develocity." 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | tasks.named("compileJava", JavaCompile::class) { 107 | sourceCompatibility = JavaVersion.VERSION_11.majorVersion 108 | targetCompatibility = JavaVersion.VERSION_11.majorVersion 109 | } 110 | 111 | tasks.processIntegrationTestResources { 112 | dependsOn(tasks.apiCheck) 113 | from(project.layout.projectDirectory.dir("api/library.api")) 114 | } 115 | 116 | tasks.named("compileKotlin", KotlinCompile::class) { 117 | compilerOptions { 118 | languageVersion = KotlinVersion.KOTLIN_1_8 119 | jvmTarget = JvmTarget.JVM_11 120 | } 121 | } 122 | 123 | tasks.withType().configureEach { 124 | systemProperty("junit.jupiter.execution.parallel.enabled", "true") 125 | systemProperty("junit.jupiter.execution.parallel.mode.default", "same_thread") 126 | val cleanupMode = System.getProperty("junit.jupiter.tempdir.cleanup.mode.default") 127 | ?: "always" 128 | systemProperty("junit.jupiter.tempdir.cleanup.mode.default", cleanupMode) 129 | } 130 | 131 | tasks.named("test") { 132 | environment = emptyMap() 133 | } 134 | 135 | tasks.named("integrationTest") { 136 | maxParallelForks = 2 137 | environment = emptyMap() 138 | } 139 | 140 | val publishUnsignedSnapshotDevelocityApiKotlinPublicationToMavenLocal by tasks.getting 141 | 142 | tasks.named("examplesTest") { 143 | maxParallelForks = 4 144 | inputs.files(files(publishUnsignedSnapshotDevelocityApiKotlinPublicationToMavenLocal)) 145 | .withPropertyName("snapshotPublicationArtifacts") 146 | .withNormalizer(ClasspathNormalizer::class) 147 | providers.environmentVariablesPrefixedBy("DEVELOCITY_API_").get().forEach { (name, value) -> 148 | inputs.property("${name}.hashCode", value.hashCode()) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/JsonParser.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example 2 | 3 | import com.squareup.moshi.Moshi 4 | import okio.buffer 5 | import okio.source 6 | import java.nio.file.Path 7 | 8 | object JsonAdapter { 9 | 10 | private val jsonAdapter = Moshi.Builder().build().adapter(Map::class.java) 11 | 12 | fun fromJson(path: Path): Map<*, *>? = 13 | jsonAdapter.fromJson(path.source().buffer()) 14 | 15 | fun toJson(map: Map<*, *>?): String = 16 | jsonAdapter.toJson(map) 17 | 18 | fun toPrettyJson(map: Map<*, *>?): String = 19 | jsonAdapter.indent(" ").toJson(map) 20 | } 21 | -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/Queries.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example 2 | 3 | object BuildStartTime { 4 | const val RECENT = "-10h" 5 | } 6 | 7 | object Queries { 8 | const val FAST = "buildStartTime>${BuildStartTime.RECENT} buildTool:gradle" 9 | } -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/Shell.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example 2 | 3 | import kotlinx.coroutines.CoroutineStart.UNDISPATCHED 4 | import kotlinx.coroutines.async 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.runBlocking 7 | import java.nio.file.Path 8 | 9 | fun runInShell(workDir: Path, vararg command: String) = 10 | runInShell(workDir, command.joinToString(" ")) 11 | 12 | fun runInShell(workDir: Path, command: String): OutputStreams { 13 | val process = ProcessBuilder("bash", "-c", command).apply { 14 | directory(workDir.toFile()) 15 | // Ensure the test's build toolchain is used (not whatever JAVA_HOME is set to) 16 | environment()["JAVA_HOME"] = System.getProperty("java.home") 17 | }.start() 18 | val streams = runBlocking { 19 | OutputStreams( 20 | stderr = async(start = UNDISPATCHED) { 21 | process.errorStream.bufferedReader().lineSequence() 22 | .onEach(System.err::println) 23 | .joinToString("\n") 24 | }.await(), 25 | stdout = async(start = UNDISPATCHED) { 26 | process.inputStream.bufferedReader().lineSequence() 27 | .onEach(System.out::println) 28 | .joinToString("\n") 29 | }.await(), 30 | ) 31 | } 32 | val exitCode = process.waitFor() 33 | check(exitCode == 0) { "Exit code '$exitCode' for command: $command" } 34 | return streams 35 | } 36 | 37 | class OutputStreams(val stdout: String, val stderr: String) 38 | -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/gradle/ExampleGradleTaskTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example.gradle 2 | 3 | import org.junit.jupiter.api.Assertions.assertFalse 4 | import org.junit.jupiter.api.Assertions.assertTrue 5 | import org.junit.jupiter.api.Test 6 | import org.junit.jupiter.api.io.TempDir 7 | import java.nio.file.Path 8 | import com.gabrielfeo.develocity.api.copyFromResources 9 | import com.gabrielfeo.develocity.api.example.BuildStartTime 10 | import com.gabrielfeo.develocity.api.example.runInShell 11 | import org.junit.jupiter.api.parallel.Execution 12 | import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT 13 | import kotlin.io.path.div 14 | 15 | @Execution(CONCURRENT) 16 | class ExampleGradleTaskTest { 17 | 18 | class TestPaths(val rootDir: Path) { 19 | val initScriptsDir = rootDir 20 | val projectDir = rootDir / "examples/example-gradle-task" 21 | } 22 | 23 | @Test 24 | fun ensureRunBuildUsesSnapshotDependency(@TempDir tempDir: Path) = with(setup(tempDir)) { 25 | val dependencies = runBuild(":buildSrc:dependencies --configuration runtimeClasspath").stdout 26 | val libraryMatches = dependencies.lines().filter { "develocity-api-kotlin" in it } 27 | assertTrue(libraryMatches.isNotEmpty()) 28 | assertTrue(libraryMatches.all { "-> SNAPSHOT" in it && "FAILED" !in it }) { 29 | "Expected forced SNAPSHOT versions, but found [${libraryMatches.joinToString(", ")}]" 30 | } 31 | } 32 | 33 | private fun setup(tempDir: Path): TestPaths { 34 | copyFromResources("/examples", tempDir) 35 | copyFromResources("/${ResourceInitScripts.FORCE_SNAPSHOT_LIBRARY}", tempDir) 36 | copyFromResources("/${ResourceInitScripts.REQUIRE_JAVA_11_COMPATIBILITY}", tempDir) 37 | return TestPaths(tempDir) 38 | } 39 | 40 | @Test 41 | fun testBuildPerformanceMetricsTask(@TempDir tempDir: Path) = with(setup(tempDir)) { 42 | val args = "--user runner --period=${BuildStartTime.RECENT}" 43 | val output = runBuild("userBuildPerformanceMetrics $args").stdout 44 | assertPerformanceMetricsOutput(output, user = "runner", period = BuildStartTime.RECENT) 45 | } 46 | 47 | @Test 48 | fun testJavaVersionCompatibility(@TempDir tempDir: Path) = with(setup(tempDir)) { 49 | val initScript = initScriptsDir / ResourceInitScripts.REQUIRE_JAVA_11_COMPATIBILITY 50 | val output = runBuild("-p buildSrc :generateExternalPluginSpecBuilders -I '$initScript'").stdout 51 | assertFalse(Regex("""FAILED|Could not resolve|No matching variant""").containsMatchIn(output)) 52 | } 53 | 54 | private fun TestPaths.runBuild(gradleArgs: String) = 55 | runInShell( 56 | projectDir, 57 | "./gradlew --stacktrace --no-daemon", 58 | "-I ${initScriptsDir / ResourceInitScripts.FORCE_SNAPSHOT_LIBRARY}", 59 | gradleArgs, 60 | ) 61 | 62 | @Suppress("SameParameterValue") 63 | private fun assertPerformanceMetricsOutput( 64 | output: String, 65 | user: String, 66 | period: String, 67 | ) { 68 | val expectedHeading = "Build performance overview for $user since $period (powered by Develocity®)" 69 | assertTrue(output.contains(expectedHeading)) 70 | assertTrue(output.contains("▶︎ Serialization factor:")) 71 | assertTrue(output.contains("⏩︎ Avoidance savings:")) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/gradle/ExampleProjectTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example.gradle 2 | 3 | import com.gabrielfeo.develocity.api.example.Queries 4 | import org.junit.jupiter.api.Assertions.assertTrue 5 | import org.junit.jupiter.api.Test 6 | import org.junit.jupiter.api.io.TempDir 7 | import java.nio.file.Path 8 | import kotlin.io.path.div 9 | import com.gabrielfeo.develocity.api.copyFromResources 10 | import com.gabrielfeo.develocity.api.example.runInShell 11 | import org.junit.jupiter.api.parallel.Execution 12 | import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT 13 | 14 | @Execution(CONCURRENT) 15 | class ExampleProjectTest { 16 | 17 | class TestPaths(val rootDir: Path) { 18 | val projectDir = rootDir / "examples/example-project" 19 | val initScriptPath = rootDir / ResourceInitScripts.FORCE_SNAPSHOT_LIBRARY 20 | } 21 | 22 | @Test 23 | fun ensureRunBuildUsesSnapshotDependencies(@TempDir tempDir: Path) = with(setup(tempDir)) { 24 | val dependencies = runBuild("dependencies --configuration runtimeClasspath").stdout 25 | val libraryMatches = dependencies.lines().filter { "develocity-api-kotlin" in it } 26 | assertTrue(libraryMatches.isNotEmpty()) 27 | assertTrue(libraryMatches.all { "-> SNAPSHOT" in it && "FAILED" !in it }) { 28 | "Expected forced SNAPSHOT versions, but found [${libraryMatches.joinToString(", ")}]" 29 | } 30 | } 31 | 32 | private fun setup(tempDir: Path): TestPaths { 33 | copyFromResources("/examples", tempDir) 34 | copyFromResources("/${ResourceInitScripts.FORCE_SNAPSHOT_LIBRARY}", tempDir) 35 | return TestPaths(tempDir) 36 | } 37 | 38 | @Test 39 | fun testExampleProject(@TempDir tempDir: Path) = with(setup(tempDir)) { 40 | val output = runBuild("""run --args '"${Queries.FAST}"'""").stdout 41 | val tableRegex = Regex("""(?ms)^[-]+\nMost frequent builds:\n\s*\n(.+\|\s*\d+\s*\n?)+""") 42 | assertTrue(tableRegex.containsMatchIn(output)) { 43 | "Expected match for pattern '$tableRegex' in output '$output'" 44 | } 45 | } 46 | 47 | private fun TestPaths.runBuild(gradleArgs: String) = 48 | runInShell( 49 | projectDir, 50 | "./gradlew --stacktrace --no-daemon", 51 | "-I $initScriptPath", 52 | gradleArgs, 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/gradle/ResourceInitScripts.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example.gradle 2 | 3 | object ResourceInitScripts { 4 | const val FORCE_SNAPSHOT_LIBRARY = "force-snapshot-library.init.gradle.kts" 5 | const val REQUIRE_JAVA_11_COMPATIBILITY = "require-java-11-compatibility.init.gradle" 6 | } 7 | -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/notebook/Jupyter.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example.notebook 2 | 3 | import com.gabrielfeo.develocity.api.copyFromResources 4 | import com.gabrielfeo.develocity.api.example.OutputStreams 5 | import com.gabrielfeo.develocity.api.example.runInShell 6 | import java.nio.file.Path 7 | import kotlin.io.path.div 8 | import kotlin.io.path.nameWithoutExtension 9 | import kotlin.io.path.notExists 10 | 11 | class Jupyter( 12 | val workDir: Path, 13 | val venv: Path, 14 | ) { 15 | class Execution( 16 | val outputStreams: OutputStreams, 17 | val outputNotebook: Path, 18 | ) 19 | 20 | fun executeNotebook(path: Path): Execution { 21 | val outputPath = path.parent / "${path.nameWithoutExtension}-executed.ipynb" 22 | val outputStreams = runInShell( 23 | workDir, 24 | "source '${venv / "bin/activate"}' &&", 25 | "jupyter nbconvert '$path'", 26 | "--to ipynb", 27 | "--execute", 28 | "--output='$outputPath'", 29 | ) 30 | return Execution(outputStreams, outputPath) 31 | } 32 | 33 | fun replacePattern( 34 | path: Path, 35 | pattern: Regex, 36 | replacement: String, 37 | ): Path { 38 | if ((workDir / "preprocessors.py").notExists()) { 39 | copyFromResources("/preprocessors.py", workDir) 40 | } 41 | val outputPath = path.parent / "${path.nameWithoutExtension}-replaced.ipynb" 42 | runInShell( 43 | workDir, 44 | "source '${venv / "bin/activate"}' &&", 45 | "jupyter nbconvert '$path'", 46 | "--to ipynb", 47 | "--output='$outputPath'", 48 | "--NotebookExporter.preprocessors=preprocessors.ReplacePatternPreprocessor", 49 | "--ReplacePatternPreprocessor.pattern='$pattern'", 50 | "--ReplacePatternPreprocessor.replacement='$replacement'", 51 | ) 52 | return outputPath 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/notebook/NotebookJson.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example.notebook 2 | 3 | class NotebookJson( 4 | val properties: Map, 5 | ) { 6 | 7 | val cells = properties["cells"] as List> 8 | 9 | val allOutputs by lazy { 10 | cells.flatMap { (it["outputs"] as? List>).orEmpty() } 11 | } 12 | 13 | val textOutputLines by lazy { 14 | allOutputs 15 | .filter { it["output_type"] == "stream" } 16 | .flatMap { it["text"] as List } 17 | } 18 | 19 | val dataframeOutputs by lazy { 20 | executeOutputsByMimeType 21 | .filter { (mimeType, _) -> mimeType == "application/kotlindataframe+json" } 22 | .map { it.value as String } 23 | } 24 | 25 | private val executeOutputsByMimeType by lazy { 26 | allOutputs 27 | .filter { it["output_type"] == "execute_result" } 28 | .flatMap { (it["data"] as Map).entries } 29 | } 30 | 31 | val kandyOutputs by lazy { 32 | executeOutputsByMimeType 33 | .filter { (mimeType, _) -> mimeType == "application/plot+json" } 34 | .map { it.value as Map } 35 | } 36 | } 37 | 38 | fun Map<*, *>?.asNotebookJson() = NotebookJson(this as Map) 39 | -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/notebook/NotebooksTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example.notebook 2 | 3 | import com.gabrielfeo.develocity.api.copyFromResources 4 | import com.gabrielfeo.develocity.api.example.JsonAdapter 5 | import com.gabrielfeo.develocity.api.example.Queries 6 | import org.junit.jupiter.api.Assertions.assertTrue 7 | import org.junit.jupiter.api.Test 8 | import org.junit.jupiter.api.assertDoesNotThrow 9 | import org.junit.jupiter.api.io.TempDir 10 | import org.junit.jupiter.api.parallel.Execution 11 | import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT 12 | import java.net.URI 13 | import java.nio.file.Path 14 | import kotlin.io.path.Path 15 | import kotlin.io.path.absolute 16 | import kotlin.io.path.div 17 | import kotlin.io.path.writeText 18 | 19 | @Execution(CONCURRENT) 20 | class NotebooksTest { 21 | 22 | @Test 23 | fun testMostFrequentBuildsNotebook(@TempDir tempDir: Path) { 24 | val jupyter = setup(tempDir) 25 | val sourceNotebook = tempDir / "examples/example-notebooks/MostFrequentBuilds.ipynb" 26 | val fasterNotebook = jupyter.forceUseOfFasterQuery(sourceNotebook) 27 | val snapshotNotebook = jupyter.forceUseOfMavenLocalSnapshotArtifact(fasterNotebook) 28 | val executedNotebook = assertDoesNotThrow { jupyter.executeNotebook(snapshotNotebook) } 29 | with(JsonAdapter.fromJson(executedNotebook.outputNotebook).asNotebookJson()) { 30 | assertTrue(textOutputLines.any { Regex("""Collected \d+ builds from the API""").containsMatchIn(it) }) { 31 | "Expected line match not found in text outputs:\n${JsonAdapter.toPrettyJson(properties)}" 32 | } 33 | assertTrue(dataframeOutputs.isNotEmpty() && dataframeOutputs.none { it.isBlank() }) { 34 | "Expected Dataframe outputs not found in notebook:\n${JsonAdapter.toPrettyJson(properties)}" 35 | } 36 | assertTrue(kandyOutputs.isNotEmpty() && kandyOutputs.none { it.isEmpty() }) { 37 | "Expected Kandy outputs not found in notebook:\n${JsonAdapter.toPrettyJson(properties)}" 38 | } 39 | } 40 | } 41 | 42 | private fun setup(tempDir: Path): Jupyter { 43 | copyFromResources("/examples", tempDir) 44 | val venv = PythonVenv(tempDir / ".venv").also { 45 | it.installRequirements( 46 | requirementsFile = tempDir / "examples/example-notebooks/requirements.txt", 47 | linksDir = Path(System.getProperty("downloaded-requirements-path")).absolute(), 48 | ) 49 | } 50 | return Jupyter(tempDir, venv.dir) 51 | } 52 | 53 | private fun Jupyter.forceUseOfFasterQuery(sourceNotebook: Path): Path = replacePattern( 54 | path = sourceNotebook, 55 | pattern = Regex("""query\s*=.+,"""), 56 | replacement = """query = "${Queries.FAST}",""", 57 | ) 58 | 59 | @Test 60 | fun testLoggingNotebook(@TempDir tempDir: Path) { 61 | val jupyter = setup(tempDir) 62 | val sourceNotebook = tempDir / "examples/example-notebooks/Logging.ipynb" 63 | val snapshotNotebook = jupyter.forceUseOfMavenLocalSnapshotArtifact(sourceNotebook) 64 | val executedNotebook = assertDoesNotThrow { jupyter.executeNotebook(snapshotNotebook) } 65 | val kernelLogs = executedNotebook.outputStreams.stderr 66 | assertTrue(kernelLogs.contains("gabrielfeo.develocity.api.Cache - HTTP cache", ignoreCase = true)) 67 | } 68 | 69 | private fun Jupyter.forceUseOfMavenLocalSnapshotArtifact(sourceNotebook: Path): Path { 70 | val mavenLocal = Path(System.getProperty("user.home"), ".m2/repository").toUri() 71 | val libraryDescriptor = (workDir / "develocity-api-kotlin.json").apply { 72 | writeText(buildLibraryDescriptor(version = "SNAPSHOT", repository = mavenLocal)) 73 | } 74 | return replacePattern( 75 | path = sourceNotebook, 76 | pattern = Regex("(?:DependsOn|%use).*develocity-api-kotlin.*"), 77 | replacement = """ 78 | %use develocity-api-kotlin@file[$libraryDescriptor] 79 | %trackClasspath on 80 | %logLevel debug 81 | """.trimIndent() 82 | ) 83 | } 84 | 85 | @Suppress("SameParameterValue") 86 | private fun buildLibraryDescriptor(version: String, repository: URI) = """ 87 | { 88 | "dependencies": ["com.gabrielfeo:develocity-api-kotlin:$version"], 89 | "repositories": ["$repository"] 90 | } 91 | """.trimIndent() 92 | } 93 | -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/notebook/PythonVenv.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example.notebook 2 | 3 | import com.gabrielfeo.develocity.api.example.runInShell 4 | import java.nio.file.Path 5 | import kotlin.io.path.absolute 6 | import kotlin.io.path.div 7 | 8 | class PythonVenv( 9 | val dir: Path, 10 | ) { 11 | 12 | init { 13 | runInShell(dir.parent, "python3 -m venv '$dir'") 14 | } 15 | 16 | fun installRequirements(requirementsFile: Path, linksDir: Path) { 17 | runInShell( 18 | dir.parent, 19 | "source '${dir / "bin/activate"}' &&", 20 | "pip install", 21 | "-r '$requirementsFile'", 22 | "--no-index", 23 | "--find-links ${linksDir.absolute()}", 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library/src/examplesTest/kotlin/com/gabrielfeo/develocity/api/example/script/ScriptsTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.example.script 2 | 3 | import com.gabrielfeo.develocity.api.copyFromResources 4 | import com.gabrielfeo.develocity.api.example.Queries 5 | import com.gabrielfeo.develocity.api.example.runInShell 6 | import org.junit.jupiter.api.Assertions.assertTrue 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.io.TempDir 10 | import java.nio.file.Path 11 | import kotlin.io.path.div 12 | 13 | class ScriptsTest { 14 | 15 | @TempDir 16 | lateinit var dir: Path 17 | 18 | @BeforeEach 19 | fun setup() { 20 | copyFromResources("/examples", dir) 21 | } 22 | 23 | @Test 24 | fun testMostFrequentBuildsScript() { 25 | val script = dir / "examples/example-scripts/example-script.main.kts" 26 | val replacedScript = forceUseOfMavenLocalSnapshotArtifact(script) 27 | val output = runInShell(dir, "kotlin '$replacedScript' '${Queries.FAST}'").stdout.trim() 28 | val tableRegex = Regex("""(?ms)^[-]+\nMost frequent builds:\n\s*\n(.+\|\s*\d+\s*\n?)+""") 29 | assertTrue(tableRegex.containsMatchIn(output)) { 30 | "Expected match for pattern '$tableRegex' in output '$output'" 31 | } 32 | } 33 | 34 | /** 35 | * Replaces the dependency declaration in the script with a SNAPSHOT version and adds the maven local repository. 36 | */ 37 | private fun forceUseOfMavenLocalSnapshotArtifact(scriptPath: Path): Path { 38 | val mavenLocal = checkNotNull(System.getProperty("user.home")).let { "$it/.m2/repository" } 39 | val scriptText = scriptPath.toFile().readText() 40 | val replaced = scriptText.replace( 41 | Regex("@file:DependsOn\\([^)]+\\)"), 42 | """ 43 | @file:DependsOn("com.gabrielfeo:develocity-api-kotlin:SNAPSHOT") 44 | @file:Repository("file://$mavenLocal") 45 | """.trimIndent(), 46 | ) 47 | val replacedPath = dir.resolve("examples/example-scripts/example-script-SNAPSHOT.main.kts") 48 | replacedPath.toFile().writeText(replaced) 49 | return replacedPath 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /library/src/examplesTest/resources/force-snapshot-library.init.gradle.kts: -------------------------------------------------------------------------------- 1 | beforeSettings{ 2 | pluginManagement { 3 | repositories { 4 | mavenCentral() 5 | gradlePluginPortal() 6 | exclusiveContent { 7 | forRepository { mavenLocal() } 8 | filter { includeGroup("com.gabrielfeo") } 9 | } 10 | } 11 | } 12 | } 13 | 14 | beforeProject { 15 | repositories { 16 | exclusiveContent { 17 | forRepository { mavenLocal() } 18 | filter { includeGroup("com.gabrielfeo") } 19 | } 20 | } 21 | } 22 | 23 | afterProject { 24 | configurations.all { 25 | resolutionStrategy { 26 | force("com.gabrielfeo:develocity-api-kotlin:SNAPSHOT") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/examplesTest/resources/preprocessors.py: -------------------------------------------------------------------------------- 1 | from nbconvert.preprocessors import Preprocessor 2 | from traitlets import Unicode 3 | import re 4 | 5 | 6 | class ReplacePatternPreprocessor(Preprocessor): 7 | """ 8 | Preprocessor that replaces lines in code cells matching a regex pattern 9 | with a replacement string, while keeping magic lines (e.g. '%use [...]') 10 | at the top, which is a requirement of the Kotlin kernel for Jupyter. 11 | The pattern and replacement can be set via config, allowing use for any regex replacement. 12 | """ 13 | 14 | pattern = Unicode().tag(config=True) 15 | replacement = Unicode().tag(config=True) 16 | 17 | def __init__(self, **kwargs): 18 | super().__init__(**kwargs) 19 | self.did_replace = False 20 | 21 | def preprocess(self, nb, resources): 22 | super().preprocess(nb, resources) 23 | if not self.did_replace: 24 | raise ValueError(f"No replacements made with pattern: {self.pattern}") 25 | return nb, resources 26 | 27 | def preprocess_cell(self, cell, resources, cell_index): 28 | # Only process code cells 29 | if cell.cell_type != "code": 30 | return cell, resources 31 | 32 | if not isinstance(cell.source, str): 33 | raise ValueError("Cell source must be a string.") 34 | 35 | regex = re.compile(self.pattern) 36 | line_magics = [] 37 | replaced = [] 38 | for line in cell.source.splitlines(keepends=True): 39 | # Replace pattern with replacement 40 | new_lines = regex.sub(self.replacement, line).splitlines(keepends=True) 41 | for new_line in new_lines: 42 | if new_line.startswith('%'): 43 | line_magics.append(new_line) 44 | else: 45 | replaced.append(new_line) 46 | 47 | new_source = "".join(line_magics + replaced) 48 | if new_source != cell.source: 49 | self.did_replace = True 50 | cell.source = new_source 51 | return cell, resources 52 | -------------------------------------------------------------------------------- /library/src/examplesTest/resources/require-java-11-compatibility.init.gradle: -------------------------------------------------------------------------------- 1 | afterProject { 2 | it.java { 3 | sourceCompatibility = JavaVersion.VERSION_11 4 | targetCompatibility = JavaVersion.VERSION_11 5 | } 6 | it.tasks.named('compileKotlin') { 7 | compilerOptions { 8 | jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /library/src/integrationTest/kotlin/com/gabrielfeo/develocity/api/DevelocityApiIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.internal.* 4 | import com.google.common.reflect.ClassPath 5 | import kotlinx.coroutines.test.runTest 6 | import okhttp3.mockwebserver.MockResponse 7 | import okhttp3.mockwebserver.MockWebServer 8 | import org.junit.jupiter.api.assertDoesNotThrow 9 | import java.net.URI 10 | import kotlin.reflect.KVisibility.PUBLIC 11 | import kotlin.reflect.full.memberProperties 12 | import kotlin.reflect.javaType 13 | import kotlin.test.* 14 | 15 | @OptIn(ExperimentalStdlibApi::class) 16 | class DevelocityApiIntegrationTest { 17 | 18 | private lateinit var mockWebServer: MockWebServer 19 | private lateinit var mockWebServerEnv: Env 20 | private val emptyEnv: Env = FakeEnv() 21 | 22 | @BeforeTest 23 | fun setUp() { 24 | mockWebServer = MockWebServer().apply { start() } 25 | mockWebServerEnv = FakeEnv( 26 | "DEVELOCITY_URL" to mockWebServer.url("/").toString(), 27 | "DEVELOCITY_ACCESS_KEY" to "${mockWebServer.url("/").host}=foo", 28 | "DEVELOCITY_CACHE_ENABLED" to "false", 29 | ) 30 | } 31 | 32 | @AfterTest 33 | fun tearDown() { 34 | mockWebServer.shutdown() 35 | } 36 | 37 | @Test 38 | fun canFetchBuildsWithEnvVarConfigAndEmptyBuildsResponse() = runTest { 39 | env = mockWebServerEnv 40 | mockWebServer.enqueue(MockResponse().setBody("[]")) 41 | val api = DevelocityApi.newInstance() 42 | val builds = api.buildsApi.getBuilds(fromInstant = 0) 43 | assertEquals(0, builds.size) 44 | api.shutdown() 45 | } 46 | 47 | @Test 48 | fun canFetchBuildsWithEnvVarConfigAndNonEmptyBuildsResponse() = runTest { 49 | val response = requireResource("/response/api/builds/5-builds.json").readText() 50 | mockWebServer.enqueue(MockResponse().setBody(response)) 51 | env = mockWebServerEnv 52 | val api = DevelocityApi.newInstance() 53 | val builds = api.buildsApi.getBuilds(fromInstant = 0) 54 | assertContentEquals( 55 | listOf( 56 | "67b3o5ld6iwc2", 57 | "e2bajrtqpe4bi", 58 | "rb5bbp6hxpcto", 59 | "gur3efx4fnqsc", 60 | "tw3yw5fhovwtq", 61 | ), 62 | builds.map { it.id }, 63 | ) 64 | api.shutdown() 65 | } 66 | 67 | @Test 68 | fun canFetchBuildsWithCodeConfig() = runTest { 69 | env = emptyEnv 70 | mockWebServer.enqueue(MockResponse().setBody("[]")) 71 | assertDoesNotThrow { 72 | val config = Config( 73 | server = mockWebServer.url("/").toUri(), 74 | accessKey = { "${mockWebServer.url("/").host}=foo" }, 75 | ) 76 | DevelocityApi.newInstance(config) 77 | } 78 | } 79 | 80 | @Test 81 | fun mainApiInterfaceExposesAllGeneratedApiClasses() = runTest { 82 | val generatedApiTypes = getGeneratedApiTypes() 83 | val mainApiInterfaceProperties = getMainApiInterfaceProperties() 84 | generatedApiTypes.forEach { 85 | mainApiInterfaceProperties.singleOrNull { type -> type == it } 86 | ?: fail("No property in DevelocityApi for $it") 87 | } 88 | } 89 | 90 | private fun getGeneratedApiTypes(): List { 91 | val cp = ClassPath.from(this::class.java.classLoader) 92 | return cp.getTopLevelClasses("com.gabrielfeo.develocity.api") 93 | .filter { it.simpleName.endsWith("Api") } 94 | .filter { !it.simpleName.endsWith("DevelocityApi") } 95 | .map { it.name } 96 | } 97 | 98 | private fun getMainApiInterfaceProperties() = DevelocityApi::class.memberProperties 99 | .filter { it.visibility == PUBLIC } 100 | .map { it.returnType.javaType.typeName } 101 | } 102 | -------------------------------------------------------------------------------- /library/src/integrationTest/kotlin/com/gabrielfeo/develocity/api/InMemoryLogRecorder.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent 4 | import ch.qos.logback.core.AppenderBase 5 | import java.util.concurrent.CopyOnWriteArrayList 6 | 7 | class InMemoryLogRecorder : AppenderBase() { 8 | 9 | val logsByLoggerName: CopyOnWriteArrayList> = CopyOnWriteArrayList() 10 | 11 | override fun append(eventObject: ILoggingEvent) { 12 | with(eventObject) { 13 | logsByLoggerName.add(loggerName to formattedMessage) 14 | } 15 | } 16 | 17 | fun clear() { 18 | logsByLoggerName.clear() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /library/src/integrationTest/kotlin/com/gabrielfeo/develocity/api/LibraryApiNamingTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import org.junit.jupiter.api.Assertions.assertTrue 4 | import org.junit.jupiter.api.Test 5 | import java.nio.file.Files 6 | import java.nio.file.Paths 7 | 8 | class LibraryApiNamingTest { 9 | 10 | /** 11 | * Tests against unfixed cases of the enum member naming bug in openapi-generator. 12 | * See PostProcessGeneratedApi.kt. 13 | */ 14 | @Test 15 | fun `all members are lower camel case`() { 16 | val libraryApi = readLibraryApi() 17 | val regex = Regex("""\b[a-z][A-Z]+\b""") 18 | val matches = regex.findAll(libraryApi).toList() 19 | assertTrue(matches.isEmpty(), "Lower camel case naming violations: ${matches.map { it.value }}") 20 | } 21 | 22 | private fun readLibraryApi(): String { 23 | val resource = this::class.java.classLoader.getResource("library.api") 24 | requireNotNull(resource) { "library.api not found in test resources" } 25 | return Files.readAllLines(Paths.get(resource.toURI())).joinToString("\n") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /library/src/integrationTest/kotlin/com/gabrielfeo/develocity/api/LoggingIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent 4 | import ch.qos.logback.core.AppenderBase 5 | import ch.qos.logback.classic.encoder.PatternLayoutEncoder 6 | import ch.qos.logback.classic.LoggerContext as LogbackLoggerContext 7 | import com.gabrielfeo.develocity.api.internal.* 8 | import kotlinx.coroutines.test.runTest 9 | import org.junit.jupiter.api.io.TempDir 10 | import org.slf4j.Logger 11 | import org.slf4j.Logger.ROOT_LOGGER_NAME 12 | import org.slf4j.LoggerFactory 13 | import java.io.File 14 | import kotlin.test.* 15 | 16 | class LoggingIntegrationTest { 17 | 18 | @TempDir 19 | lateinit var tempDir: File 20 | 21 | private val recorder: InMemoryLogRecorder by lazy { 22 | val lc = LoggerFactory.getILoggerFactory() as LogbackLoggerContext 23 | // The appender is attached to the com.gabrielfeo.develocity.api logger 24 | val logger = lc.getLogger("com.gabrielfeo.develocity") 25 | logger.getAppender("IN_MEMORY") as InMemoryLogRecorder 26 | } 27 | 28 | private lateinit var api: DevelocityApi 29 | 30 | @BeforeTest 31 | fun setup() { 32 | // Appender is configured via logback-test.xml; ensure it's started 33 | recorder.start() 34 | val mockWebServer = okhttp3.mockwebserver.MockWebServer() 35 | mockWebServer.enqueue(okhttp3.mockwebserver.MockResponse().setBody("[]")) 36 | mockWebServer.start() 37 | env = FakeEnv() 38 | api = DevelocityApi.newInstance( 39 | config = Config( 40 | server = mockWebServer.url("/").toUri(), 41 | accessKey = { "${mockWebServer.url("/").host}=foo" }, 42 | cacheConfig = Config.CacheConfig( 43 | cacheEnabled = true, 44 | cacheDir = tempDir, 45 | ) 46 | ) 47 | ) 48 | } 49 | 50 | @AfterTest 51 | fun tearDown() { 52 | val lc = LoggerFactory.getILoggerFactory() as LogbackLoggerContext 53 | val logger = lc.getLogger("com.gabrielfeo.develocity") 54 | logger.detachAppender("IN_MEMORY") 55 | recorder.stop() 56 | api.shutdown() 57 | } 58 | 59 | @Test 60 | fun logsUnderLibraryPackage() = runTest { 61 | api.buildsApi.getBuilds(since = 0, maxBuilds = 1) 62 | with(recorder.logsByLoggerName) { 63 | assertTrue(isNotEmpty()) 64 | assertTrue(any { (_, message) -> message.contains("cache dir", ignoreCase = true) }) 65 | assertTrue(any { (_, message) -> message.contains("cache miss", ignoreCase = true) }) 66 | assertTrue(any { (_, message) -> message.contains("get", ignoreCase = true) }) 67 | forEach { (loggerName, message) -> 68 | assertTrue( 69 | loggerName.startsWith("com.gabrielfeo.develocity.api"), 70 | "Log from unexpected logger: '$loggerName' with message '$message'" 71 | ) 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /library/src/integrationTest/kotlin/com/gabrielfeo/develocity/api/SmokeTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.internal.RealEnv 4 | import com.gabrielfeo.develocity.api.internal.asMap 5 | import kotlin.test.Test 6 | import kotlin.test.assertTrue 7 | 8 | class SmokeTest { 9 | 10 | @Test 11 | fun testSuiteEnvironmentIsEmpty() { 12 | val vars = RealEnv.asMap().toMutableMap().entries.apply { 13 | // Added by either Gradle or JUnit that but irrelevant to library functionality 14 | removeAll { (k, _) -> k.endsWith("CF_USER_TEXT_ENCODING") } 15 | removeAll { (k, _) -> k.startsWith("JAVA_MAIN_CLASS") } 16 | } 17 | assertTrue(vars.isEmpty(), "Expected empty environment, found $vars") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /library/src/integrationTest/kotlin/com/gabrielfeo/develocity/api/extension/BuildsApiExtensionsIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.extension 2 | 3 | import com.gabrielfeo.develocity.api.* 4 | import com.gabrielfeo.develocity.api.internal.* 5 | import com.gabrielfeo.develocity.api.model.BuildModelName 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.flow.take 8 | import kotlinx.coroutines.test.runTest 9 | import okhttp3.Request 10 | import org.junit.jupiter.api.* 11 | import org.junit.jupiter.api.Assertions.* 12 | import kotlin.test.AfterTest 13 | import kotlin.test.BeforeTest 14 | import kotlin.time.Duration.Companion.minutes 15 | 16 | class BuildsApiExtensionsIntegrationTest { 17 | 18 | private val recorder = RequestRecorder() 19 | private lateinit var api: DevelocityApi 20 | private lateinit var mockWebServer: okhttp3.mockwebserver.MockWebServer 21 | 22 | @BeforeTest 23 | fun setup() { 24 | mockWebServer = okhttp3.mockwebserver.MockWebServer() 25 | mockWebServer.enqueue(okhttp3.mockwebserver.MockResponse().setBody("[]")) 26 | mockWebServer.start() 27 | env = FakeEnv() 28 | api = buildApi(recorder) 29 | } 30 | 31 | @AfterTest 32 | fun teardown() { 33 | api.shutdown() 34 | mockWebServer.shutdown() 35 | } 36 | 37 | @Test 38 | fun getBuildsFlowPreservesParamsAcrossRequests() = runTest(timeout = 6.minutes) { 39 | api.buildsApi.getBuildsFlow( 40 | since = 0, 41 | query = "user:*", 42 | models = listOf(BuildModelName.gradleAttributes), 43 | allModels = true, 44 | reverse = true, 45 | buildsPerPage = 2, 46 | ).take(4).collect() 47 | recorder.requests.forEach { 48 | assertUrlParam(it, "query", "user:*") 49 | assertUrlParam(it, "models", "gradle-attributes") 50 | assertUrlParam(it, "allModels", "true") 51 | assertUrlParam(it, "reverse", "true") 52 | } 53 | } 54 | 55 | @Test 56 | fun getBuildsFlowReplacesSinceForFromBuildAfterFirstRequest() = runTest { 57 | api.buildsApi.getBuildsFlow(since = 1, buildsPerPage = 2).take(10).collect() 58 | assertReplacedForFromBuildAfterFirstRequest(param = "since" to "1") 59 | } 60 | 61 | @Test 62 | fun getBuildsFlowReplacesFromInstantForFromBuildAfterFirstRequest() = runTest { 63 | api.buildsApi.getBuildsFlow(fromInstant = 1, buildsPerPage = 2).take(10).collect() 64 | assertReplacedForFromBuildAfterFirstRequest(param = "fromInstant" to "1") 65 | } 66 | 67 | private fun assertReplacedForFromBuildAfterFirstRequest(param: Pair) { 68 | with(recorder.requests) { 69 | val (key, value) = param 70 | first().let { 71 | assertUrlParam(it, key, value) 72 | assertUrlParam(it, "fromBuild", null) 73 | } 74 | (this - first()).forEach { 75 | assertUrlParam(it, key, null) 76 | assertUrlParamNotNull(it, "fromBuild") 77 | } 78 | } 79 | } 80 | 81 | private fun buildApi(recorder: RequestRecorder) = 82 | DevelocityApi.newInstance( 83 | config = Config( 84 | server = mockWebServer.url("/").toUri(), 85 | accessKey = { "${mockWebServer.url("/").host}=foo" }, 86 | clientBuilder = recorder.clientBuilder(), 87 | cacheConfig = Config.CacheConfig(cacheEnabled = false), 88 | ) 89 | ) 90 | 91 | private fun assertUrlParam(request: Request, key: String, expected: String?) { 92 | val actual = request.url.queryParameter(key) 93 | assertEquals(expected, actual, "Expected '$key='$expected', but was '$key=$actual' (${request.url})") 94 | } 95 | 96 | private fun assertUrlParamNotNull(request: Request, key: String) { 97 | assertNotNull(request.url.queryParameter(key), "Expected param $key, but was null (${request.url})") 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /library/src/integrationTest/kotlin/com/gabrielfeo/develocity/api/extension/RequestRecorder.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.extension 2 | 3 | import okhttp3.OkHttpClient 4 | import okhttp3.Request 5 | import java.util.* 6 | 7 | class RequestRecorder { 8 | 9 | val requests = LinkedList() 10 | 11 | fun clientBuilder() = OkHttpClient.Builder() 12 | .addNetworkInterceptor { 13 | requests += it.request() 14 | it.proceed(it.request()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /library/src/integrationTest/kotlin/com/gabrielfeo/develocity/api/internal/jupyter/DevelocityApiJupyterIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal.jupyter 2 | 3 | import com.google.common.reflect.ClassPath 4 | import com.google.common.reflect.ClassPath.ClassInfo 5 | import org.intellij.lang.annotations.Language 6 | import org.jetbrains.kotlinx.jupyter.api.Code 7 | import org.jetbrains.kotlinx.jupyter.testkit.JupyterReplTestCase 8 | import org.junit.jupiter.api.parallel.Execution 9 | import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT 10 | import kotlin.reflect.KVisibility 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.test.assertNotEquals 14 | 15 | @ExperimentalStdlibApi 16 | @Execution(CONCURRENT) 17 | class DevelocityApiJupyterIntegrationTest : JupyterReplTestCase() { 18 | 19 | @Test 20 | fun `imports all extensions`() = assertSucceeds(""" 21 | com.gabrielfeo.develocity.api.BuildsApi::getGradleAttributesFlow 22 | com.gabrielfeo.develocity.api.BuildsApi::getBuildsFlow 23 | 24 | val attrs = emptyList() 25 | "custom value name" in attrs 26 | attrs["custom value name"] 27 | """) 28 | 29 | @Test 30 | fun `Given default clientBuilder, re-uses OkHttpClient resources`() { 31 | val config = """Config(server = java.net.URI("https://foo.com"), accessKey = { "foo.com=bar" })""" 32 | execSuccess("""val api = DevelocityApi.newInstance($config)""") 33 | execSuccess("""val api2 = DevelocityApi.newInstance($config)""") 34 | val connectionPool1 = execRendered("api.config.clientBuilder.build().connectionPool.hashCode()") 35 | val connectionPool2 = execRendered("api2.config.clientBuilder.build().connectionPool.hashCode()") 36 | assertEquals(connectionPool1, connectionPool2) 37 | } 38 | 39 | @Test 40 | fun `Given custom clientBuilder set, does not re-use OkHttpClient resources`() { 41 | val config = """ 42 | Config( 43 | server = java.net.URI("https://foo.com"), 44 | accessKey = { "foo.com=bar" }, 45 | clientBuilder = okhttp3.OkHttpClient.Builder(), 46 | ) 47 | """.trimIndent() 48 | execSuccess("val api = DevelocityApi.newInstance($config)") 49 | execSuccess("val api2 = DevelocityApi.newInstance($config)") 50 | val connectionPool1 = execRendered("api.config.clientBuilder.build().connectionPool.hashCode()") 51 | val connectionPool2 = execRendered("api2.config.clientBuilder.build().connectionPool.hashCode()") 52 | assertNotEquals(connectionPool1, connectionPool2) 53 | } 54 | 55 | @Test 56 | fun `imports all public classes`() { 57 | val classes = allPublicClassesRecursive("com.gabrielfeo.develocity.api") 58 | val references = classes.joinToString("\n") { "${it.name}::class" } 59 | println("Running code:\n$references") 60 | assertSucceeds(references) 61 | } 62 | 63 | @Suppress("SameParameterValue") 64 | private fun allPublicClassesRecursive(packageName: String): List { 65 | val cp = ClassPath.from(this::class.java.classLoader) 66 | return cp.getTopLevelClassesRecursive(packageName) 67 | .filter { "internal" !in it.packageName } 68 | .filter { !it.name.endsWith("Kt") } 69 | .filter { Class.forName(it.name).kotlin.visibility == KVisibility.PUBLIC } 70 | } 71 | 72 | private fun assertSucceeds(@Language("kts") code: Code) { 73 | code.lines().forEach { 74 | execRendered(it) 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /library/src/integrationTest/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /library/src/integrationTest/resources/response/api/builds/5-builds.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "67b3o5ld6iwc2", 4 | "availableAt": 1754424892386, 5 | "buildToolType": "gradle", 6 | "buildToolVersion": "9.0.0", 7 | "buildAgentVersion": "4.1" 8 | }, 9 | { 10 | "id": "e2bajrtqpe4bi", 11 | "availableAt": 1754425137741, 12 | "buildToolType": "gradle", 13 | "buildToolVersion": "9.0.0", 14 | "buildAgentVersion": "4.1" 15 | }, 16 | { 17 | "id": "rb5bbp6hxpcto", 18 | "availableAt": 1754425141088, 19 | "buildToolType": "gradle", 20 | "buildToolVersion": "9.0.0", 21 | "buildAgentVersion": "4.1" 22 | }, 23 | { 24 | "id": "gur3efx4fnqsc", 25 | "availableAt": 1757168772917, 26 | "buildToolType": "gradle", 27 | "buildToolVersion": "8.14.3", 28 | "buildAgentVersion": "4.1.1" 29 | }, 30 | { 31 | "id": "tw3yw5fhovwtq", 32 | "availableAt": 1757168910932, 33 | "buildToolType": "gradle", 34 | "buildToolVersion": "8.14.3", 35 | "buildAgentVersion": "4.1.1" 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/DevelocityApi.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.internal.buildOkHttpClient 4 | import com.gabrielfeo.develocity.api.internal.buildRetrofit 5 | import com.gabrielfeo.develocity.api.internal.infrastructure.Serializer 6 | import okhttp3.OkHttpClient 7 | import retrofit2.Retrofit 8 | import retrofit2.create 9 | 10 | /** 11 | * Develocity API client. API endpoints are grouped exactly as in the 12 | * [Develocity API Manual](https://docs.gradle.com/enterprise/api-manual/#reference_documentation): 13 | * 14 | * - [buildsApi] 15 | * - [buildCacheApi] 16 | * - [metaApi] 17 | * - [testDistributionApi] 18 | * 19 | * Create an instance with [newInstance]: 20 | * 21 | * ```kotlin 22 | * val api = DevelocityApi.newInstance() 23 | * api.buildsApi.getBuilds(...) 24 | * ``` 25 | * 26 | * You may pass a default [Config], e.g. for sharing [OkHttpClient] resources: 27 | * 28 | * ```kotlin 29 | * val options = Options(clientBuilder = myOwnOkHttpClient.newBuilder()) 30 | * val api = DevelocityApi.newInstance(options) 31 | * api.buildsApi.getBuilds(...) 32 | * ``` 33 | */ 34 | interface DevelocityApi { 35 | 36 | val authApi: AuthApi 37 | val buildsApi: BuildsApi 38 | val buildCacheApi: BuildCacheApi 39 | val projectsApi: ProjectsApi 40 | val testsApi: TestsApi 41 | val metaApi: MetaApi 42 | val testDistributionApi: TestDistributionApi 43 | 44 | /** 45 | * Library configuration options. 46 | */ 47 | val config: Config 48 | 49 | /** 50 | * Release resources allowing the program to finish before the internal client's idle timeout. 51 | */ 52 | fun shutdown() 53 | 54 | companion object { 55 | 56 | /** 57 | * Create a new instance of `DevelocityApi` with a custom `Config`. 58 | */ 59 | fun newInstance(config: Config = Config()): DevelocityApi { 60 | return RealDevelocityApi(config) 61 | } 62 | } 63 | 64 | } 65 | 66 | internal class RealDevelocityApi( 67 | override val config: Config, 68 | ) : DevelocityApi { 69 | 70 | private val okHttpClient by lazy { 71 | buildOkHttpClient(config = config) 72 | } 73 | 74 | private val retrofit: Retrofit by lazy { 75 | buildRetrofit( 76 | config, 77 | okHttpClient, 78 | Serializer.moshi, 79 | ) 80 | } 81 | 82 | override val authApi: AuthApi by lazy { retrofit.create() } 83 | override val buildsApi: BuildsApi by lazy { retrofit.create() } 84 | override val buildCacheApi: BuildCacheApi by lazy { retrofit.create() } 85 | override val projectsApi: ProjectsApi by lazy { retrofit.create() } 86 | override val testsApi: TestsApi by lazy { retrofit.create() } 87 | override val metaApi: MetaApi by lazy { retrofit.create() } 88 | override val testDistributionApi: TestDistributionApi by lazy { retrofit.create() } 89 | 90 | override fun shutdown() { 91 | okHttpClient.run { 92 | dispatcher.executorService.shutdown() 93 | connectionPool.evictAll() 94 | cache?.close() 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/extension/BuildAttributesValueExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.extension 2 | 3 | import com.gabrielfeo.develocity.api.model.BuildAttributesValue 4 | 5 | operator fun List.get(name: String): String? { 6 | return find { it.name == name }?.value 7 | } 8 | 9 | operator fun List.contains(name: String): Boolean { 10 | return any { it.name == name } 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/extension/BuildsApiExtensions.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.gabrielfeo.develocity.api.extension 4 | 5 | import com.gabrielfeo.develocity.api.Config 6 | import com.gabrielfeo.develocity.api.BuildsApi 7 | import com.gabrielfeo.develocity.api.internal.API_MAX_BUILDS 8 | import com.gabrielfeo.develocity.api.model.* 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.DelicateCoroutinesApi 11 | import kotlinx.coroutines.GlobalScope 12 | import kotlinx.coroutines.flow.* 13 | 14 | /** 15 | * Gets builds on demand from the API, in as many requests as necessary. It allows 16 | * for queries of any size, as opposed to [BuildsApi.getBuilds] which is limited by the 17 | * API itself to 1000. 18 | * 19 | * - Will request from the API until results end, collection stops or an error occurs. 20 | * - Parameters same as [BuildsApi.getBuilds]. 21 | * - Using [query] is highly recommended for server-side filtering (equivalent to Develocity advanced 22 | * query). 23 | * - `maxBuilds` is the only unsupported parameter, because this Flow will instead fetch 24 | * continously. Use [Flow.take] to stop collecting at a specific count. 25 | */ 26 | fun BuildsApi.getBuildsFlow( 27 | since: Long? = null, 28 | sinceBuild: String? = null, 29 | fromInstant: Long? = null, 30 | fromBuild: String? = null, 31 | query: String? = null, 32 | reverse: Boolean? = null, 33 | maxWaitSecs: Int? = null, 34 | buildsPerPage: Int = API_MAX_BUILDS, 35 | models: List? = null, 36 | allModels: Boolean? = false, 37 | ): Flow { 38 | return flow { 39 | var builds = getBuilds( 40 | since = since, 41 | sinceBuild = sinceBuild, 42 | fromInstant = fromInstant, 43 | fromBuild = fromBuild, 44 | query = query, 45 | reverse = reverse, 46 | maxWaitSecs = maxWaitSecs, 47 | maxBuilds = buildsPerPage, 48 | models = models, 49 | allModels = allModels, 50 | ) 51 | emitAll(builds.asFlow()) 52 | while (builds.isNotEmpty()) { 53 | builds = getBuilds( 54 | fromBuild = builds.last().id, 55 | query = query, 56 | reverse = reverse, 57 | maxWaitSecs = maxWaitSecs, 58 | maxBuilds = buildsPerPage, 59 | models = models, 60 | allModels = allModels, 61 | ) 62 | emitAll(builds.asFlow()) 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * Gets [GradleAttributes] of all builds from a given date. Queries [BuildsApi.getBuilds] first, 69 | * the endpoint providing a timeline of builds, then maps each to [BuildsApi.getGradleAttributes]. 70 | * 71 | * Instead of filtering builds downstream based on `GradleAttributes` (e.g. using [Flow.filter]), 72 | * prefer filtering server-side using a `query` (see [BuildsApi.getBuilds]). 73 | * 74 | * ### Buffering 75 | * 76 | * Will request eagerly and buffer up to [Int.MAX_VALUE] calls. 77 | * 78 | * ### Concurrency 79 | * 80 | * Attributes are requested concurrently in coroutines started in [scope]. The number of 81 | * concurrent requests underneath is still limited by [Config.maxConcurrentRequests]. 82 | * 83 | * @param scope CoroutineScope in which to create coroutines. Defaults to [GlobalScope]. 84 | */ 85 | @Deprecated( 86 | "Use `getBuildsFlow(models = listOf(BuildModelName.gradleAttributes))` instead. " + 87 | "This function will be removed in the next release.", 88 | replaceWith = ReplaceWith( 89 | "getBuildsFlow(since, sinceBuild, fromInstant, fromBuild, query, reverse," + 90 | "maxWaitSecs, models = listOf(BuildModelName.gradleAttributes))", 91 | imports = [ 92 | "com.gabrielfeo.develocity.api.extension.getBuildsFlow", 93 | "com.gabrielfeo.develocity.api.model.BuildModelName", 94 | ] 95 | ), 96 | ) 97 | @OptIn(DelicateCoroutinesApi::class) 98 | fun BuildsApi.getGradleAttributesFlow( 99 | since: Long = 0, 100 | sinceBuild: String? = null, 101 | fromInstant: Long? = null, 102 | fromBuild: String? = null, 103 | query: String? = null, 104 | reverse: Boolean? = null, 105 | maxWaitSecs: Int? = null, 106 | scope: CoroutineScope = GlobalScope, 107 | models: List? = null, 108 | ): Flow = 109 | getBuildsFlow( 110 | since = since, 111 | sinceBuild = sinceBuild, 112 | fromInstant = fromInstant, 113 | fromBuild = fromBuild, 114 | query = query, 115 | reverse = reverse, 116 | maxWaitSecs = maxWaitSecs, 117 | models = models, 118 | ).withGradleAttributes(scope, api = this).map { (_, attrs) -> 119 | attrs 120 | } 121 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/extension/Mapping.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.extension 2 | 3 | import com.gabrielfeo.develocity.api.* 4 | import com.gabrielfeo.develocity.api.model.* 5 | import kotlinx.coroutines.* 6 | import kotlinx.coroutines.flow.* 7 | 8 | /** 9 | * Joins builds with their [GradleAttributes], which comes from a different endpoint 10 | * ([BuildsApi.getGradleAttributes]). 11 | * 12 | * Don't expect client-side filtering to be efficient. Does as many concurrent calls 13 | * as it can, requesting attributes in an eager coroutine, in [scope]. 14 | */ 15 | internal fun Flow.withGradleAttributes( 16 | scope: CoroutineScope, 17 | api: BuildsApi, 18 | ): Flow> = 19 | map { build -> 20 | build to scope.async { 21 | api.getGradleAttributes(build.id) 22 | } 23 | }.buffer(Int.MAX_VALUE).map { (build, attrs) -> 24 | build to attrs.await() 25 | } 26 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/ApiConstants.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal 2 | 3 | /** 4 | * Undocumented max value of `/api/builds?maxBuilds`. Last checked in 2022.4. 5 | */ 6 | internal const val API_MAX_BUILDS = 1000 7 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/Env.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal 2 | 3 | internal var env: Env = RealEnv 4 | 5 | internal interface Env { 6 | operator fun get(name: String): String? 7 | } 8 | 9 | internal object RealEnv : Env { 10 | override fun get(name: String): String? = System.getenv(name) 11 | } 12 | 13 | @Suppress("UnusedReceiverParameter") 14 | internal fun RealEnv.asMap(): Map = System.getenv() 15 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/OkHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal 2 | 3 | import com.gabrielfeo.develocity.api.* 4 | import com.gabrielfeo.develocity.api.internal.auth.* 5 | import com.gabrielfeo.develocity.api.internal.caching.* 6 | import okhttp3.Cache 7 | import okhttp3.OkHttpClient 8 | import okhttp3.logging.HttpLoggingInterceptor 9 | import okhttp3.logging.HttpLoggingInterceptor.Level.BASIC 10 | import okhttp3.logging.HttpLoggingInterceptor.Level.BODY 11 | import org.slf4j.Logger 12 | import org.slf4j.LoggerFactory 13 | import java.time.Duration 14 | 15 | private const val HTTP_LOGGER_NAME = "com.gabrielfeo.develocity.api.OkHttpClient" 16 | private const val CACHE_LOGGER_NAME = "com.gabrielfeo.develocity.api.Cache" 17 | 18 | /** 19 | * Builds the final `OkHttpClient` with a `Config`. 20 | */ 21 | internal fun buildOkHttpClient( 22 | config: Config, 23 | ) = with(config.clientBuilder) { 24 | readTimeout(Duration.ofMillis(config.readTimeoutMillis)) 25 | val httpLogger = LoggerFactory.getLogger(HTTP_LOGGER_NAME) 26 | val cacheLogger = LoggerFactory.getLogger(CACHE_LOGGER_NAME) 27 | if (config.cacheConfig.cacheEnabled) { 28 | cache(buildCache(config, cacheLogger)) 29 | } else { 30 | cacheLogger.debug("HTTP cache is disabled") 31 | } 32 | addInterceptors(config, cacheLogger) 33 | addNetworkInterceptors(config, httpLogger) 34 | build().apply { 35 | config.maxConcurrentRequests?.let { 36 | dispatcher.maxRequests = it 37 | dispatcher.maxRequestsPerHost = it 38 | } 39 | } 40 | } 41 | 42 | private fun OkHttpClient.Builder.addInterceptors( 43 | config: Config, 44 | cacheLogger: Logger, 45 | ) { 46 | if (config.cacheConfig.cacheEnabled) { 47 | addInterceptor(CacheHitLoggingInterceptor(cacheLogger)) 48 | } 49 | } 50 | 51 | private fun OkHttpClient.Builder.addNetworkInterceptors( 52 | config: Config, 53 | httpLogger: Logger, 54 | ) { 55 | if (config.cacheConfig.cacheEnabled) { 56 | addNetworkInterceptor(buildCacheEnforcingInterceptor(config)) 57 | } 58 | addNetworkInterceptor(HttpLoggingInterceptor(logger = httpLogger::debug).apply { level = BASIC }) 59 | addNetworkInterceptor(HttpLoggingInterceptor(logger = httpLogger::trace).apply { level = BODY }) 60 | // Add authentication after logging to prevent clients from leaking their access key 61 | addNetworkInterceptor(HttpBearerAuth("bearer", config.accessKey())) 62 | } 63 | 64 | internal fun buildCache( 65 | config: Config, 66 | cacheLogger: Logger, 67 | ): Cache { 68 | val cacheDir = config.cacheConfig.cacheDir 69 | val maxSize = config.cacheConfig.maxCacheSize 70 | cacheLogger.debug("HTTP cache dir: {} (max {}B)", cacheDir, maxSize) 71 | return Cache(cacheDir, maxSize) 72 | } 73 | 74 | private fun buildCacheEnforcingInterceptor( 75 | config: Config, 76 | ) = CacheEnforcingInterceptor( 77 | longTermCacheUrlPattern = config.cacheConfig.longTermCacheUrlPattern, 78 | longTermCacheMaxAge = config.cacheConfig.longTermCacheMaxAge, 79 | shortTermCacheUrlPattern = config.cacheConfig.shortTermCacheUrlPattern, 80 | shortTermCacheMaxAge = config.cacheConfig.shortTermCacheMaxAge, 81 | ) 82 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/OkHttpClientBuilderFactory.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal 2 | 3 | import okhttp3.OkHttpClient 4 | 5 | interface OkHttpClientBuilderFactory { 6 | 7 | companion object { 8 | var default: OkHttpClientBuilderFactory = FreshOkHttpClientBuilderFactory() 9 | } 10 | 11 | fun create(): OkHttpClient.Builder 12 | } 13 | 14 | /** 15 | * Creates a new [OkHttpClient.Builder] instance for every call to [create]. 16 | */ 17 | class FreshOkHttpClientBuilderFactory : OkHttpClientBuilderFactory { 18 | override fun create() = OkHttpClient.Builder() 19 | } 20 | 21 | /** 22 | * Re-uses the same [OkHttpClient.Builder] instance for every call to [create], allowing for 23 | * internal resources such as connection pools and threads to be shared between clients. 24 | */ 25 | class SharedOkHttpClientBuilderFactory : OkHttpClientBuilderFactory { 26 | private val sharedClient: OkHttpClient by lazy { OkHttpClient.Builder().build() } 27 | override fun create() = sharedClient.newBuilder() 28 | } 29 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/Retrofit.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal 2 | 3 | import com.gabrielfeo.develocity.api.* 4 | import com.squareup.moshi.Moshi 5 | import okhttp3.OkHttpClient 6 | import retrofit2.Retrofit 7 | import retrofit2.converter.moshi.MoshiConverterFactory 8 | import retrofit2.converter.scalars.ScalarsConverterFactory 9 | 10 | internal fun buildRetrofit( 11 | config: Config, 12 | client: OkHttpClient, 13 | moshi: Moshi, 14 | ) = with(Retrofit.Builder()) { 15 | baseUrl(config.server.resolve("/").toString()) 16 | addConverterFactory(ScalarsConverterFactory.create()) 17 | addConverterFactory(MoshiConverterFactory.create(moshi)) 18 | client(client) 19 | build() 20 | } 21 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/SystemProperties.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal 2 | 3 | internal var systemProperties: SystemProperties = RealSystemProperties 4 | 5 | internal interface SystemProperties { 6 | val userHome: String? 7 | } 8 | 9 | internal object RealSystemProperties : SystemProperties { 10 | override val userHome: String? = System.getProperty("user.home") 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/auth/AccessKeyResolver.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal.auth 2 | 3 | import com.gabrielfeo.develocity.api.internal.Env 4 | import com.gabrielfeo.develocity.api.internal.env 5 | import com.gabrielfeo.develocity.api.internal.systemProperties 6 | import okio.FileSystem 7 | import okio.Path 8 | import okio.Path.Companion.toPath 9 | 10 | internal var accessKeyResolver = AccessKeyResolver( 11 | env, 12 | homeDirectory = checkNotNull(systemProperties.userHome).toPath(), 13 | fileSystem = FileSystem.SYSTEM, 14 | ) 15 | 16 | internal class AccessKeyResolver( 17 | private val env: Env, 18 | private val homeDirectory: Path, 19 | private val fileSystem: FileSystem, 20 | ) { 21 | 22 | private val gradleUserHome: Path 23 | get() = env["GRADLE_USER_HOME"]?.toPath() ?: (homeDirectory / ".gradle") 24 | 25 | fun resolve(host: String): String? { 26 | val keyEntry = fromEnvVar("DEVELOCITY_ACCESS_KEY", host) 27 | ?: fromEnvVar("GRADLE_ENTERPRISE_ACCESS_KEY", host) 28 | ?: fromFile(gradleUserHome / "develocity/keys.properties", host) 29 | ?: fromFile(homeDirectory / ".m2/.develocity/keys.properties", host) 30 | return keyEntry?.accessKey 31 | } 32 | 33 | private fun fromEnvVar(varName: String, host: String): HostAccessKeyEntry? = 34 | env[varName]?.let { envVar -> 35 | envVar.split(';') 36 | .firstNotNullOfOrNull { entry -> 37 | if (entry.isBlank()) null 38 | else HostAccessKeyEntry(entry).takeIf { it.host == host } 39 | } 40 | } 41 | 42 | private fun fromFile(path: Path, host: String): HostAccessKeyEntry? { 43 | if (!fileSystem.exists(path)) return null 44 | fileSystem.read(path) { 45 | while (true) { 46 | val line = readUtf8Line()?.trim(' ') ?: break 47 | if (line.isBlank() || line.isComment()) continue 48 | val entry = HostAccessKeyEntry(line) 49 | if (entry.host == host) return entry 50 | } 51 | } 52 | return null 53 | } 54 | 55 | private fun String.isComment() = startsWith('#') 56 | } 57 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/auth/HostAccessKeyEntry.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal.auth 2 | 3 | internal class HostAccessKeyEntry(entry: String) { 4 | 5 | private val components = entry.substringBefore(" #").trim().split('=') 6 | 7 | init { 8 | require(components.size == 2 && host.isNotBlank() && accessKey.isNotBlank()) { 9 | "Invalid access key entry format: '${redact(entry)}'. Expected format is 'host=accessKey'." 10 | } 11 | } 12 | 13 | val host: String get() = components[0] 14 | val accessKey: String get() = components[1] 15 | } 16 | 17 | private const val REDACTED_MAX_LENGTH = 5 18 | 19 | private fun redact(entry: String): String = 20 | if (entry.length <= REDACTED_MAX_LENGTH) entry 21 | else "${entry.substring(0, REDACTED_MAX_LENGTH - 1)}[redacted]" 22 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/caching/CacheEnforcingInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal.caching 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.Request 5 | import okhttp3.Response 6 | 7 | 8 | internal class CacheEnforcingInterceptor( 9 | private val longTermCacheUrlPattern: Regex, 10 | private val longTermCacheMaxAge: Long, 11 | private val shortTermCacheUrlPattern: Regex, 12 | private val shortTermCacheMaxAge: Long, 13 | ) : Interceptor { 14 | 15 | override fun intercept(chain: Interceptor.Chain): Response { 16 | val response = chain.proceed(chain.request()) 17 | val maxAge = maxAgeFor(response.request) 18 | if (maxAge <= 0) { 19 | return response 20 | } 21 | return response.newBuilder() 22 | .header("cache-control", "max-age=$maxAge") 23 | .removeHeader("pragma") 24 | .removeHeader("expires") 25 | .removeHeader("vary") 26 | .build() 27 | } 28 | 29 | private fun maxAgeFor(request: Request): Long { 30 | val url = request.url.toString() 31 | return when { 32 | longTermCacheUrlPattern.matches(url) -> longTermCacheMaxAge 33 | shortTermCacheUrlPattern.matches(url) -> shortTermCacheMaxAge 34 | else -> 0 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/caching/CacheHitLoggingInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal.caching 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.Response 5 | import org.slf4j.Logger 6 | 7 | 8 | internal class CacheHitLoggingInterceptor( 9 | private val logger: Logger, 10 | ) : Interceptor { 11 | 12 | override fun intercept(chain: Interceptor.Chain): Response { 13 | val url = chain.request().url 14 | val response = chain.proceed(chain.request()) 15 | val wasHit = with(response) { cacheResponse != null && networkResponse == null } 16 | val hitOrMiss = if (wasHit) "hit" else "miss" 17 | logger.debug("Cache {}: {}", hitOrMiss, url) 18 | return response 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/jupyter/DevelocityApiJupyterIntegration.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal.jupyter 2 | 3 | import org.jetbrains.kotlinx.jupyter.api.ExecutionCallback 4 | import org.jetbrains.kotlinx.jupyter.api.KotlinKernelVersion 5 | import org.jetbrains.kotlinx.jupyter.api.libraries.LibraryDefinition 6 | 7 | @Suppress("unused") 8 | class DevelocityApiJupyterIntegration : LibraryDefinition { 9 | 10 | override val minKernelVersion = KotlinKernelVersion.from("0.12.0.217") 11 | 12 | override val imports = listOf( 13 | "com.gabrielfeo.develocity.api.*", 14 | "com.gabrielfeo.develocity.api.model.*", 15 | "com.gabrielfeo.develocity.api.extension.*", 16 | ) 17 | 18 | override val init: List> = listOf( 19 | { 20 | execute(""" 21 | com.gabrielfeo.develocity.api.internal.OkHttpClientBuilderFactory.default = 22 | com.gabrielfeo.develocity.api.internal.SharedOkHttpClientBuilderFactory() 23 | """.trimIndent()) 24 | } 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /library/src/main/resources/META-INF/kotlin-jupyter-libraries/libraries.json: -------------------------------------------------------------------------------- 1 | { 2 | "producers": [], 3 | "definitions": [ 4 | { 5 | "fqn": "com.gabrielfeo.develocity.api.internal.jupyter.DevelocityApiJupyterIntegration" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/CacheConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import kotlin.test.* 4 | 5 | class CacheConfigTest { 6 | 7 | @Test 8 | fun `default longTermCacheUrlPattern matches attributes URLs`() { 9 | Config.CacheConfig().longTermCacheUrlPattern.assertMatches( 10 | "https://ge.gradle.org/api/builds/tgnsqkb2rhlni/gradle-attributes", 11 | "https://ge.gradle.org/api/builds/tgnsqkb2rhlni/maven-attributes", 12 | ) 13 | } 14 | 15 | @Test 16 | fun `default longTermCacheUrlPattern matches build cache performance URLs`() { 17 | Config.CacheConfig().longTermCacheUrlPattern.assertMatches( 18 | "https://ge.gradle.org/api/builds/tgnsqkb2rhlni/gradle-build-cache-performance", 19 | "https://ge.gradle.org/api/builds/tgnsqkb2rhlni/maven-build-cache-performance", 20 | ) 21 | } 22 | 23 | @Test 24 | fun `default shortTermCacheUrlPattern matches builds URLs`() { 25 | Config.CacheConfig().shortTermCacheUrlPattern.assertMatches( 26 | "https://ge.gradle.org/api/builds?since=0", 27 | "https://ge.gradle.org/api/builds?since=0&maxBuilds=2", 28 | ) 29 | } 30 | 31 | private fun Regex.assertMatches(vararg values: String) { 32 | values.forEach { 33 | assertTrue(matches(it), "/$pattern/ doesn't match '$it'") 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/ConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.internal.* 4 | import com.gabrielfeo.develocity.api.internal.auth.* 5 | import okio.Path.Companion.toPath 6 | import okio.fakefilesystem.FakeFileSystem 7 | import org.junit.jupiter.api.DynamicTest.dynamicTest 8 | import org.junit.jupiter.api.TestFactory 9 | import org.junit.jupiter.api.assertDoesNotThrow 10 | import java.net.URI 11 | import kotlin.test.* 12 | 13 | class ConfigTest { 14 | 15 | @BeforeTest 16 | fun before() { 17 | env = FakeEnv("DEVELOCITY_URL" to "https://example.com/") 18 | systemProperties = FakeSystemProperties() 19 | accessKeyResolver = AccessKeyResolver( 20 | env, 21 | homeDirectory = "/home/testuser".toPath(), 22 | fileSystem = FakeFileSystem(), 23 | ) 24 | } 25 | 26 | @Test 27 | fun `Given no URL set in env, error`() { 28 | env = FakeEnv() 29 | assertFailsWith { 30 | Config() 31 | } 32 | } 33 | 34 | @Test 35 | fun `Given server URL set in env, server is correct URL`() { 36 | (env as FakeEnv)["DEVELOCITY_URL"] = "https://example.com/" 37 | assertEquals(URI("https://example.com/"), Config().server) 38 | } 39 | 40 | @Test 41 | fun `Given server URL set in code, server is correct URL`() { 42 | val config = Config(server = URI("https://custom.example.com/")) 43 | assertEquals(URI("https://custom.example.com/"), config.server) 44 | } 45 | 46 | @TestFactory 47 | fun `Given malformed URL, error`() = listOf( 48 | "mailto:foo@bar.com", 49 | "file:///example/foo", 50 | "http://example.com?foo", 51 | "https://example.com?foo", 52 | "https://example.com/foo", 53 | "https://example.com/foo?bar=1", 54 | "https://example.com/foo/bar/baz", 55 | "https://example.com/foo/bar/baz?qux=1", 56 | ).map { url -> 57 | dynamicTest(url) { 58 | assertFailsWith { 59 | Config(server = URI(url)) 60 | } 61 | } 62 | } 63 | 64 | @Test 65 | fun `Given default access key function and resolvable key, accessKey is key`() { 66 | (env as FakeEnv)["DEVELOCITY_URL"] = "https://example.com/" 67 | (env as FakeEnv)["DEVELOCITY_ACCESS_KEY"] = "example.com=foo" 68 | assertEquals("foo", Config().accessKey()) 69 | } 70 | 71 | @Test 72 | fun `Given default access key function and no resolvable key, error`() { 73 | (env as FakeEnv)["DEVELOCITY_URL"] = "https://example.com/" 74 | (env as FakeEnv)["DEVELOCITY_ACCESS_KEY"] = "notexample.com=foo" 75 | assertFailsWith { 76 | Config().accessKey() 77 | } 78 | } 79 | 80 | @Test 81 | fun `Given custom access key function fails, uncaught and unwrapped error`() { 82 | val error = assertFails { 83 | Config(accessKey = { throw RuntimeException("foo") }).accessKey() 84 | } 85 | assertIs(error) 86 | assertEquals("foo", error.message) 87 | } 88 | 89 | @Test 90 | fun `Given custom access key function yields value, accessKey is value`() { 91 | assertEquals("foo", Config(accessKey = { "foo" }).accessKey()) 92 | } 93 | 94 | @Test 95 | fun `maxConcurrentRequests accepts int`() { 96 | (env as FakeEnv)["DEVELOCITY_API_MAX_CONCURRENT_REQUESTS"] = "1" 97 | assertDoesNotThrow { 98 | Config().maxConcurrentRequests 99 | } 100 | } 101 | 102 | @Test 103 | fun `Given timeout set in env, readTimeoutMillis returns env value`() { 104 | (env as FakeEnv)["DEVELOCITY_API_READ_TIMEOUT_MILLIS"] = "100000" 105 | assertEquals(100_000L, Config().readTimeoutMillis) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/DevelocityApiExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.extension.getBuildsFlow 4 | import com.gabrielfeo.develocity.api.extension.getGradleAttributesFlow 5 | import com.gabrielfeo.develocity.api.model.FakeBuild 6 | import kotlinx.coroutines.* 7 | import kotlinx.coroutines.flow.* 8 | import kotlinx.coroutines.test.runTest 9 | import org.junit.jupiter.api.Test 10 | import kotlin.test.assertEquals 11 | import kotlin.time.Duration.Companion.seconds 12 | 13 | @OptIn(ExperimentalCoroutinesApi::class) 14 | class DevelocityApiExtensionsTest { 15 | 16 | private val api = FakeBuildsApi( 17 | builds = listOf( 18 | FakeBuild(id = "a", availableAt = 1), 19 | FakeBuild(id = "b", availableAt = 2), 20 | FakeBuild(id = "c", availableAt = 3), 21 | FakeBuild(id = "d", availableAt = 4), 22 | FakeBuild(id = "e", availableAt = 5), 23 | FakeBuild(id = "f", availableAt = 6), 24 | ) 25 | ) 26 | 27 | @Test 28 | // TODO In this case, the 2nd request could be avoided 29 | fun `Given single page, getBuildsFlow calls API twice and emits all builds`() = runTest { 30 | val builds = api.getBuildsFlow(since = 0, buildsPerPage = 1000).toList() 31 | check(api.builds.size < 1000) 32 | // On 2nd time gets empty and completes 33 | assertEquals(2, api.getBuildsCallCount.value) 34 | assertEquals(6, builds.size) 35 | } 36 | 37 | @Test 38 | fun `Given 3 pages, getBuildsFlow calls API 4 times and emits all builds`() = runTest { 39 | check(api.builds.size == 6) 40 | val builds = api.getBuildsFlow(since = 0, buildsPerPage = 2).toList() 41 | // On 4th time gets empty list and completes 42 | assertEquals(4, api.getBuildsCallCount.value) 43 | assertEquals(6, builds.size) 44 | } 45 | 46 | @Test 47 | fun `getGradleAttributesFlow calls getGradleAttributes once per build, eagerly`() = runTest { 48 | backgroundScope.launch { 49 | api.getGradleAttributesFlow(scope = this).collect { 50 | // Make the first collect never complete, simulating a slow collector 51 | Job().join() 52 | } 53 | } 54 | // Expect one eager call per build despite slow collector 55 | withTimeoutOrNull(2.seconds) { 56 | api.getGradleAttributesCallCount.take(api.builds.size).collect() 57 | } 58 | assertEquals(api.builds.size, api.getGradleAttributesCallCount.value) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/DevelocityApiTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.internal.* 4 | import org.junit.jupiter.api.assertDoesNotThrow 5 | import org.junit.jupiter.api.assertThrows 6 | import kotlin.test.* 7 | 8 | class DevelocityApiTest { 9 | 10 | @Test 11 | fun `Fails eagerly if no API URL`() { 12 | env = FakeEnv() 13 | val error = assertThrows { 14 | DevelocityApi.newInstance(Config()) 15 | } 16 | error.assertRootMessageContains("DEVELOCITY_URL") 17 | } 18 | 19 | @Test 20 | fun `Fails lazily if no access key`() { 21 | env = FakeEnv("DEVELOCITY_URL" to "https://example.com/") 22 | val api = assertDoesNotThrow { 23 | DevelocityApi.newInstance(Config()) 24 | } 25 | val error = assertThrows { 26 | api.buildsApi.toString() 27 | } 28 | error.assertRootMessageContains("DEVELOCITY_ACCESS_KEY") 29 | } 30 | 31 | private fun Throwable.assertRootMessageContains(text: String) { 32 | cause?.assertRootMessageContains(text) ?: assertContains(message.orEmpty(), text) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/FakeBuildsApi.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.model.* 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | import retrofit2.http.Query 6 | 7 | class FakeBuildsApi( 8 | val builds: List, 9 | ) : FakeBuildsApiScaffold { 10 | 11 | val getBuildsCallCount = MutableStateFlow(0) 12 | val getGradleAttributesCallCount = MutableStateFlow(0) 13 | 14 | override suspend fun getBuilds( 15 | fromInstant: Long?, 16 | fromBuild: String?, 17 | reverse: Boolean?, 18 | maxBuilds: Int?, 19 | maxWaitSecs: Int?, 20 | query: String?, 21 | models: List?, 22 | allModels: Boolean?, 23 | since: Long?, 24 | sinceBuild: String?, 25 | ): List { 26 | getBuildsCallCount.value++ 27 | check((reverse ?: maxWaitSecs ?: query ?: models) == null) { "Not supported" } 28 | if ((fromBuild ?: sinceBuild) != null) { 29 | check((since ?: fromInstant) == null) { "Invalid request" } 30 | } 31 | if (since != null) { 32 | check(since == 0L) { "Filtering by date is not implemented" } 33 | check((fromBuild ?: sinceBuild) == null) { "Invalid request" } 34 | } 35 | if (fromInstant != null) { 36 | check(fromInstant == 0L) { "Filtering by date is not implemented" } 37 | check((fromBuild ?: sinceBuild) == null) { "Invalid request" } 38 | } 39 | checkNotNull(maxBuilds) 40 | val first = builds.indexOfFirst { it.id == (fromBuild ?: sinceBuild) } + 1 41 | return builds 42 | .slice(first..builds.lastIndex) 43 | .take(maxBuilds) 44 | } 45 | 46 | override suspend fun getGradleAttributes( 47 | id: String, 48 | availabilityWaitTimeoutSecs: Int?, 49 | ): GradleAttributes { 50 | getGradleAttributesCallCount.value++ 51 | val attrs = readFromJsonResource("gradle-attributes-response.json") 52 | return attrs.copy(id = id) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/OkHttpClientTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.internal.* 4 | import com.gabrielfeo.develocity.api.internal.auth.AccessKeyResolver 5 | import com.gabrielfeo.develocity.api.internal.auth.HttpBearerAuth 6 | import com.gabrielfeo.develocity.api.internal.auth.accessKeyResolver 7 | import com.gabrielfeo.develocity.api.internal.caching.CacheEnforcingInterceptor 8 | import com.gabrielfeo.develocity.api.internal.caching.CacheHitLoggingInterceptor 9 | import okhttp3.Dispatcher 10 | import okhttp3.OkHttpClient 11 | import okio.Path.Companion.toPath 12 | import okio.fakefilesystem.FakeFileSystem 13 | import kotlin.test.* 14 | 15 | class OkHttpClientTest { 16 | 17 | @Test 18 | fun `Adds authentication`() { 19 | val client = buildClient() 20 | assertTrue(client.networkInterceptors.any { it is HttpBearerAuth }) 21 | } 22 | 23 | @Test 24 | fun `Given maxConcurrentRequests, sets values in Dispatcher`() { 25 | val client = buildClient( 26 | "DEVELOCITY_API_MAX_CONCURRENT_REQUESTS" to "123" 27 | ) 28 | assertEquals(123, client.dispatcher.maxRequests) 29 | assertEquals(123, client.dispatcher.maxRequestsPerHost) 30 | } 31 | 32 | @Test 33 | fun `Given no maxConcurrentRequests, preserves original client's Dispatcher values`() { 34 | val baseClient = OkHttpClient.Builder() 35 | .dispatcher( 36 | Dispatcher().apply { 37 | maxRequests = 1 38 | maxRequestsPerHost = 1 39 | } 40 | ).build() 41 | val client = buildClient(clientBuilder = baseClient.newBuilder()) 42 | assertEquals(1, client.dispatcher.maxRequests) 43 | assertEquals(1, client.dispatcher.maxRequestsPerHost) 44 | } 45 | 46 | @Test 47 | fun `Given cache enabled, configures caching`() { 48 | val client = buildClient("DEVELOCITY_API_CACHE_ENABLED" to "true") 49 | assertTrue(client.networkInterceptors.any { it is CacheEnforcingInterceptor }) 50 | assertNotNull(client.cache) 51 | } 52 | 53 | @Test 54 | fun `Given cache disabled, no caching or cache logging`() { 55 | val client = buildClient("DEVELOCITY_API_CACHE_ENABLED" to "false") 56 | assertTrue(client.networkInterceptors.none { it is CacheEnforcingInterceptor }) 57 | assertTrue(client.interceptors.none { it is CacheHitLoggingInterceptor }) 58 | assertNull(client.cache) 59 | } 60 | 61 | @Test 62 | fun `Increases read timeout`() { 63 | val client = buildClient() 64 | val defaultTimeout = OkHttpClient.Builder().build().readTimeoutMillis 65 | assertTrue(client.readTimeoutMillis > defaultTimeout) 66 | } 67 | 68 | /** 69 | * Tests against regressions of issue gabrielfeo/develocity-api-kotlin#451 70 | */ 71 | @Test 72 | fun `Given no clientBuilder specified, OkHttpClient resources not re-used`() { 73 | assertNotEquals(buildClient().connectionPool, buildClient().connectionPool) 74 | } 75 | 76 | private fun buildClient( 77 | vararg envVars: Pair, 78 | clientBuilder: OkHttpClient.Builder? = null, 79 | ): OkHttpClient { 80 | val fakeEnv = FakeEnv(*envVars) 81 | if ("DEVELOCITY_ACCESS_KEY" !in fakeEnv) 82 | fakeEnv["DEVELOCITY_ACCESS_KEY"] = "example.com=example-token" 83 | if ("DEVELOCITY_URL" !in fakeEnv) 84 | fakeEnv["DEVELOCITY_URL"] = "https://example.com/" 85 | env = fakeEnv 86 | systemProperties = FakeSystemProperties() 87 | accessKeyResolver = AccessKeyResolver( 88 | env, 89 | homeDirectory = "/home/testuser".toPath(), 90 | fileSystem = FakeFileSystem(), 91 | ) 92 | val config = when (clientBuilder) { 93 | null -> Config() 94 | else -> Config(clientBuilder = clientBuilder) 95 | } 96 | return buildOkHttpClient(config) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/RetrofitTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.internal.* 4 | import com.gabrielfeo.develocity.api.internal.auth.* 5 | import com.squareup.moshi.Moshi 6 | import okio.Path.Companion.toPath 7 | import okio.fakefilesystem.FakeFileSystem 8 | import retrofit2.Retrofit 9 | import kotlin.test.* 10 | 11 | class RetrofitTest { 12 | 13 | @Test 14 | fun `Sets instance URL from options`() { 15 | val retrofit = buildRetrofit( 16 | "DEVELOCITY_URL" to "https://example.com/", 17 | ) 18 | assertEquals("https://example.com/", retrofit.baseUrl().toString()) 19 | } 20 | 21 | /** 22 | * This prevents Retrofit from failing with a trailing slash requirement, 23 | * ensuring the library is compatible with a DEVELOCITY_URL value that may 24 | * have been set for official tooling such as the Develocity Python agent, 25 | * which doesn't require a trailing slash. 26 | */ 27 | @Test 28 | fun `Ensures trailing slash in URL`() { 29 | val retrofit = buildRetrofit( 30 | "DEVELOCITY_URL" to "https://example.com", 31 | ) 32 | assertEquals("https://example.com/", retrofit.baseUrl().toString()) 33 | } 34 | 35 | private fun buildRetrofit( 36 | vararg envVars: Pair, 37 | ): Retrofit { 38 | val fakeEnv = FakeEnv(*envVars) 39 | if ("DEVELOCITY_ACCESS_KEY" !in fakeEnv) 40 | fakeEnv["DEVELOCITY_ACCESS_KEY"] = "example.com=example-token" 41 | if ("DEVELOCITY_URL" !in fakeEnv) 42 | fakeEnv["DEVELOCITY_URL"] = "https://example.com/" 43 | env = fakeEnv 44 | systemProperties = FakeSystemProperties() 45 | accessKeyResolver = AccessKeyResolver( 46 | env, 47 | homeDirectory = "/home/testuser".toPath(), 48 | fileSystem = FakeFileSystem(), 49 | ) 50 | val config = Config() 51 | return buildRetrofit( 52 | config = config, 53 | client = buildOkHttpClient(config), 54 | moshi = Moshi.Builder().build() 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/TestResourceUtils.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.internal.infrastructure.Serializer 4 | import com.squareup.moshi.adapter 5 | import okio.buffer 6 | import okio.source 7 | 8 | @OptIn(ExperimentalStdlibApi::class) 9 | inline fun readFromJsonResource(name: String): T { 10 | val adapter = Serializer.moshi.adapter() 11 | val classLoader = T::class.java.classLoader 12 | val jsonSource = checkNotNull(classLoader.getResourceAsStream(name)).source().buffer() 13 | val obj = adapter.fromJson(jsonSource) 14 | return requireNotNull(obj) { 15 | "JSON resource $name is null" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/extension/BuildsApiExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.extension 2 | 3 | import com.gabrielfeo.develocity.api.FakeBuildsApi 4 | import com.gabrielfeo.develocity.api.model.Build 5 | import com.gabrielfeo.develocity.api.model.FakeBuild 6 | import kotlinx.coroutines.channels.Channel 7 | import kotlinx.coroutines.flow.launchIn 8 | import kotlinx.coroutines.flow.onEach 9 | import kotlinx.coroutines.test.runTest 10 | import org.junit.jupiter.api.RepeatedTest 11 | import org.junit.jupiter.api.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.test.assertTrue 14 | 15 | class BuildsApiExtensionsTest { 16 | 17 | private val api = FakeBuildsApi( 18 | builds = listOf( 19 | FakeBuild(id = "a", availableAt = 1), 20 | FakeBuild(id = "b", availableAt = 2), 21 | FakeBuild(id = "c", availableAt = 3), 22 | FakeBuild(id = "d", availableAt = 4), 23 | FakeBuild(id = "e", availableAt = 5), 24 | ) 25 | ) 26 | 27 | @Test 28 | fun `getBuildsFlow with low maxBuilds pages until empty response`() = runTest { 29 | val channel = Channel(Channel.RENDEZVOUS) 30 | api.getBuildsFlow(buildsPerPage = 2) 31 | .onEach { channel.send(it) } 32 | .launchIn(this) 33 | // Collect page 1, expecting 1 request so far 34 | assertEquals(api.builds[0], channel.receive()) 35 | assertEquals(1, api.getBuildsCallCount.value) 36 | assertEquals(api.builds[1], channel.receive()) 37 | // Page 1 exhausted. Collect page 2 expecting new request 38 | assertEquals(api.builds[2], channel.receive()) 39 | assertEquals(2, api.getBuildsCallCount.value) 40 | assertEquals(api.builds[3], channel.receive()) 41 | // Page 2 exhausted. Collect page 3 expecting 2 new requests (last is empty) 42 | assertEquals(api.builds[4], channel.receive()) 43 | assertTrue(channel.tryReceive().isFailure) 44 | assertEquals(4, api.getBuildsCallCount.value) 45 | channel.close() 46 | } 47 | 48 | @Test 49 | fun `getBuildsFlow with high maxBuilds pages until empty response`() = runTest { 50 | val channel = Channel(Channel.RENDEZVOUS) 51 | api.getBuildsFlow(buildsPerPage = api.builds.size) 52 | .onEach { channel.send(it) } 53 | .launchIn(this) 54 | // Collect page 1 (with all builds), expecting 1 request so far 55 | assertEquals(api.builds[0], channel.receive()) 56 | assertEquals(1, api.getBuildsCallCount.value) 57 | assertEquals(api.builds[1], channel.receive()) 58 | assertEquals(api.builds[2], channel.receive()) 59 | assertEquals(api.builds[3], channel.receive()) 60 | assertEquals(api.builds[4], channel.receive()) 61 | // Page 1 exhausted. Expect no more builds, despite new request 62 | assertTrue(channel.tryReceive().isFailure) 63 | assertEquals(2, api.getBuildsCallCount.value) 64 | channel.close() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/extension/MappingTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.extension 2 | 3 | import com.gabrielfeo.develocity.api.FakeBuildsApi 4 | import com.gabrielfeo.develocity.api.model.FakeBuild 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.Job 7 | import kotlinx.coroutines.flow.asFlow 8 | import kotlinx.coroutines.flow.collect 9 | import kotlinx.coroutines.flow.take 10 | import kotlinx.coroutines.flow.toList 11 | import kotlinx.coroutines.launch 12 | import kotlinx.coroutines.test.runTest 13 | import kotlinx.coroutines.withTimeoutOrNull 14 | import org.junit.jupiter.api.Test 15 | import kotlin.test.assertEquals 16 | import kotlin.time.Duration.Companion.seconds 17 | 18 | class MappingTest { 19 | 20 | private val api = FakeBuildsApi( 21 | builds = listOf( 22 | FakeBuild(id = "a", availableAt = 1), 23 | FakeBuild(id = "b", availableAt = 2), 24 | FakeBuild(id = "c", availableAt = 3), 25 | FakeBuild(id = "d", availableAt = 4), 26 | FakeBuild(id = "e", availableAt = 5), 27 | ) 28 | ) 29 | 30 | @Test 31 | fun `withGradleAttributes pairs each build with its GradleAttributes`() = runTest { 32 | val buildsToAttrs = api.builds.asFlow().withGradleAttributes(scope = this, api).toList() 33 | assertEquals(5, api.getGradleAttributesCallCount.value) 34 | assertEquals(5, buildsToAttrs.size) 35 | buildsToAttrs.forEach { (build, attrs) -> 36 | assertEquals(build.id, attrs.id) 37 | } 38 | } 39 | 40 | @Test 41 | fun `withGradleAttributes fetches GradleAttributes for all builds eagerly`() = runTest { 42 | backgroundScope.launch { 43 | api.builds.asFlow().withGradleAttributes(scope = this, api).collect { 44 | // Make the first collect never complete, simulating a slow collector 45 | Job().join() 46 | } 47 | } 48 | // Expect 5 eager calls despite slow collector 49 | withTimeoutOrNull(2.seconds) { 50 | api.getGradleAttributesCallCount.take(5).collect() 51 | } 52 | assertEquals(5, api.getGradleAttributesCallCount.value) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/internal/auth/AccessKeyResolverTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal.auth 2 | 3 | import com.gabrielfeo.develocity.api.internal.FakeEnv 4 | import okio.Path 5 | import okio.Path.Companion.toPath 6 | import okio.fakefilesystem.FakeFileSystem 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.params.ParameterizedTest 9 | import org.junit.jupiter.params.provider.MethodSource 10 | import kotlin.test.assertEquals 11 | 12 | 13 | private val host = "host.example.com" 14 | private val home = "/home/testuser".toPath() 15 | 16 | class AccessKeyResolverTest { 17 | 18 | data class FileCase(val path: Path, val content: String, val expected: String?) 19 | data class EnvVarCase(val varName: String, val value: String?, val expected: String?) 20 | 21 | private lateinit var env: FakeEnv 22 | private lateinit var fileSystem: FakeFileSystem 23 | private lateinit var resolver: AccessKeyResolver 24 | 25 | @BeforeEach 26 | fun setUp() { 27 | env = FakeEnv() 28 | fileSystem = FakeFileSystem() 29 | resolver = AccessKeyResolver(env, home, fileSystem) 30 | } 31 | 32 | private fun writeKeysFile(path: Path, content: String) { 33 | fileSystem.createDirectories(path.parent!!) 34 | fileSystem.write(path) { writeUtf8(content) } 35 | } 36 | 37 | companion object { 38 | 39 | @JvmStatic 40 | fun standardFileCaseProvider() = listOf( 41 | "/home/testuser/.gradle/develocity/keys.properties".toPath(), 42 | "/home/testuser/.m2/.develocity/keys.properties".toPath(), 43 | ).flatMap { 44 | listOf( 45 | FileCase(it, content = "$host=foo\n", expected = "foo"), 46 | FileCase(it, content = "$host=foo", expected = "foo"), 47 | FileCase(it, content = "$host=foo # comment", expected = "foo"), 48 | FileCase(it, content = "$host=foo # comment", expected = "foo"), 49 | FileCase(it, content = "other=bar\n$host=foo\nnot$host=baz\n", expected = "foo"), 50 | FileCase(it, content = "\n#foo\n\nother=bar\n\n$host=foo\nnot$host=baz\n", expected = "foo"), 51 | FileCase(it, content = "", expected = null), 52 | FileCase(it, content = "\n", expected = null), 53 | FileCase(it, content = "other=bar\nnot$host=baz\n", expected = null), 54 | ) 55 | } 56 | 57 | @JvmStatic 58 | fun customGradleUserHomeCaseProvider() = standardFileCaseProvider() 59 | 60 | @JvmStatic 61 | fun envVarCaseProvider() = listOf( 62 | "DEVELOCITY_ACCESS_KEY", 63 | "GRADLE_ENTERPRISE_ACCESS_KEY", 64 | ).flatMap { 65 | listOf( 66 | EnvVarCase(it, "$host=foo", expected = "foo"), 67 | EnvVarCase(it, ";$host=foo;", expected = "foo"), 68 | EnvVarCase(it, "other=bar;$host=foo;not$host=baz", expected = "foo"), 69 | EnvVarCase(it, "", expected = null), 70 | EnvVarCase(it, ";", expected = null), 71 | EnvVarCase(it, "other=bar;not$host=baz", expected = null), 72 | ) 73 | } 74 | } 75 | 76 | @ParameterizedTest(name = "example") 77 | @MethodSource("standardFileCaseProvider") 78 | fun resolveFromStandardFile(case: FileCase) { 79 | writeKeysFile(case.path, case.content) 80 | assertEquals(case.expected, resolver.resolve(host)) 81 | } 82 | 83 | @ParameterizedTest 84 | @MethodSource("customGradleUserHomeCaseProvider") 85 | fun resolveFromFileOnCustomGradleUserHome(case: FileCase) { 86 | val customHome = "/custom/gradle/user/home".toPath() 87 | env["GRADLE_USER_HOME"] = customHome.toString() 88 | writeKeysFile(customHome / "develocity/keys.properties", case.content) 89 | assertEquals(case.expected, resolver.resolve(host)) 90 | } 91 | 92 | @ParameterizedTest 93 | @MethodSource("envVarCaseProvider") 94 | fun resolveFromEnvVar(case: EnvVarCase) { 95 | env[case.varName] = case.value 96 | assertEquals(case.expected, resolver.resolve(host)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/internal/caching/CacheEnforcingInterceptorTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal.caching 2 | 3 | import okhttp3.OkHttpClient 4 | import okhttp3.Request 5 | import okhttp3.mockwebserver.MockResponse 6 | import okhttp3.mockwebserver.MockWebServer 7 | import org.junit.jupiter.api.Test 8 | import kotlin.test.BeforeTest 9 | import kotlin.test.assertEquals 10 | import kotlin.test.assertNull 11 | 12 | private const val SHORT_TERM_CACHE_MAX_AGE = 1L 13 | private const val LONG_TERM_CACHE_MAX_AGE = 2L 14 | 15 | class CacheEnforcingInterceptorTest { 16 | 17 | private val interceptor = CacheEnforcingInterceptor( 18 | shortTermCacheUrlPattern = Regex(".*short.*"), 19 | shortTermCacheMaxAge = SHORT_TERM_CACHE_MAX_AGE, 20 | longTermCacheUrlPattern = Regex(".*long.*"), 21 | longTermCacheMaxAge = LONG_TERM_CACHE_MAX_AGE, 22 | ) 23 | 24 | private val server = MockWebServer() 25 | 26 | private val client = OkHttpClient.Builder() 27 | .addNetworkInterceptor(interceptor) 28 | .build() 29 | 30 | private val originalResponse = MockResponse() 31 | .setHeader("cache-control", "no-cache, no-store, must-revalidate") 32 | .setHeader("expires", "0") 33 | .setHeader("pragma", "no-cache") 34 | .setHeader("vary", "origin") 35 | 36 | @BeforeTest 37 | fun enqueueResponse() { 38 | server.enqueue(originalResponse) 39 | } 40 | 41 | @Test 42 | fun `URL matched for short-term cache`() { 43 | val response = client.newCall(buildRequest("/short")).execute() 44 | assertEquals("max-age=$SHORT_TERM_CACHE_MAX_AGE", response.headers["cache-control"]) 45 | assertNull(response.headers["pragma"]) 46 | assertNull(response.headers["expiry"]) 47 | assertNull(response.headers["vary"]) 48 | } 49 | 50 | @Test 51 | fun `URL matched for long-term cache`() { 52 | val response = client.newCall(buildRequest("/long")).execute() 53 | assertEquals("max-age=$LONG_TERM_CACHE_MAX_AGE", response.headers["cache-control"]) 54 | assertNull(response.headers["pragma"]) 55 | assertNull(response.headers["expiry"]) 56 | assertNull(response.headers["vary"]) 57 | } 58 | 59 | @Test 60 | fun `URL not matched for caching`() { 61 | val response = client.newCall(buildRequest("/other")).execute() 62 | assertEquals(originalResponse.headers, response.headers) 63 | } 64 | 65 | private fun buildRequest(path: String) = Request.Builder() 66 | .get() 67 | .url(server.url(path)) 68 | .build() 69 | } 70 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/model/BuildAttributesValueExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.model 2 | 3 | import com.gabrielfeo.develocity.api.extension.contains 4 | import com.gabrielfeo.develocity.api.extension.get 5 | import org.junit.jupiter.api.Test 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertTrue 8 | 9 | class BuildAttributesValueExtensionsTest { 10 | 11 | @Test 12 | fun get() { 13 | val list = listOf( 14 | BuildAttributesValue(name = "foo", "bar"), 15 | BuildAttributesValue(name = "bar", "foo"), 16 | ) 17 | assertEquals("bar", list["foo"]) 18 | } 19 | 20 | @Test 21 | fun contains() { 22 | val list = listOf( 23 | BuildAttributesValue(name = "foo", "bar"), 24 | ) 25 | assertTrue("foo" in list) 26 | assertTrue("bar" !in list) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/gabrielfeo/develocity/api/model/FakeBuild.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.model 2 | 3 | @Suppress("TestFunctionName") 4 | fun FakeBuild(id: String, availableAt: Long) = Build( 5 | id = id, 6 | availableAt = availableAt, 7 | buildToolType = "", 8 | buildToolVersion = "", 9 | buildAgentVersion = "", 10 | ) 11 | -------------------------------------------------------------------------------- /library/src/test/resources/gradle-attributes-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "psa5uc3o2hebc", 3 | "buildStartTime": 1655856017507, 4 | "buildDuration": 232754, 5 | "gradleVersion": "7.4.1", 6 | "pluginVersion": "3.8.1", 7 | "rootProjectName": "gradle", 8 | "requestedTasks": [], 9 | "hasFailed": false, 10 | "tags": [ 11 | "LOCAL", 12 | "Android Studio", 13 | "Dirty", 14 | "feature/example", 15 | "IDE sync", 16 | "Mac OS X" 17 | ], 18 | "values": [ 19 | { 20 | "name": "Android Studio version", 21 | "value": "2021.1.1 Patch 3" 22 | }, 23 | { 24 | "name": "Build Cache: Local", 25 | "value": "Enabled" 26 | }, 27 | { 28 | "name": "Build Cache: Remote", 29 | "value": "Enabled" 30 | }, 31 | { 32 | "name": "Build secrets migration done", 33 | "value": "true" 34 | }, 35 | { 36 | "name": "CPU: Cores", 37 | "value": "12" 38 | }, 39 | { 40 | "name": "CPU: Name", 41 | "value": "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz" 42 | }, 43 | { 44 | "name": "CPU: Usage [Begin]", 45 | "value": "42.53% user, 23.56% sys, 33.89% idle " 46 | }, 47 | { 48 | "name": "CPU: Usage [End]", 49 | "value": "21.90% user, 19.56% sys, 58.53% idle " 50 | }, 51 | { 52 | "name": "Displays: Names", 53 | "value": "Color LCD, LG HDR WFHD" 54 | }, 55 | { 56 | "name": "Displays: Total", 57 | "value": "2" 58 | }, 59 | { 60 | "name": "Email", 61 | "value": "example@github.com" 62 | }, 63 | { 64 | "name": "Git branch", 65 | "value": "feature/example" 66 | }, 67 | { 68 | "name": "Git commit id", 69 | "value": "d92128a2124b2330f7d0f72386ee122838d383b2" 70 | }, 71 | { 72 | "name": "Git commit id short", 73 | "value": "d92128a2" 74 | }, 75 | { 76 | "name": "Git repository", 77 | "value": "https://github.com/gradle/gradle.git" 78 | }, 79 | { 80 | "name": "Gradle: StartParameter", 81 | "value": "StartParameter{taskRequests=[DefaultTaskExecutionRequest{args=[],projectPath='null',rootDir='null'}], excludedTaskNames=[], currentDir=/Users/example/projects/gradle, projectDir=/Users/example/projects/gradle, projectProperties={android.injected.build.model.only.advanced=true, android.injected.build.model.disable.src.download=true, org.gradle.kotlin.dsl.provider.cid=83559353704077, android.injected.invoked.from.ide=true, android.injected.build.model.only.versioned=3, android.injected.studio.version=2021.1.1 Patch 3, idea.gradle.do.not.build.tasks=true, android.injected.build.model.only=true}, systemPropertiesArgs={idea.active=true, java.awt.headless=true, idea.sync.active=true, org.gradle.internal.GradleProjectBuilderOptions=omit_all_tasks, idea.version=2021.1}, gradleUserHomeDir=/Users/example/.gradle, gradleHome=/Users/example/.gradle/wrapper/dists/gradle-7.4.1-all/htb4l60oox3i12rnwfoqoo8d/gradle-7.4.1, logLevel=LIFECYCLE, showStacktrace=ALWAYS, buildFile=null, initScripts=[/private/var/folders/b1/xhx7v4215k58r3ct3hf201d80000gn/T/ijmapper.gradle, /private/var/folders/b1/xhx7v4215k58r3ct3hf201d80000gn/T/sync.studio.tooling.gradle, /private/var/folders/b1/xhx7v4215k58r3ct3hf201d80000gn/T/ijinit.gradle], dryRun=false, rerunTasks=false, offline=false, refreshDependencies=false, parallelProjectExecution=true, configureOnDemand=true, maxWorkerCount=12, buildCacheEnabled=true, writeDependencyLocks=false, verificationMode=STRICT, refreshKeys=false}" 82 | }, 83 | { 84 | "name": "JVM: Available Processors", 85 | "value": "12" 86 | }, 87 | { 88 | "name": "JVM: Free Memory [Begin]", 89 | "value": "4978M" 90 | }, 91 | { 92 | "name": "JVM: Free Memory [End]", 93 | "value": "1978M" 94 | }, 95 | { 96 | "name": "JVM: Max Memory", 97 | "value": "5120M" 98 | }, 99 | { 100 | "name": "JVM: Total Memory [Begin]", 101 | "value": "5120M" 102 | }, 103 | { 104 | "name": "JVM: Total Memory [End]", 105 | "value": "5120M" 106 | }, 107 | { 108 | "name": "Memory: Swap Usage [Begin]", 109 | "value": "5679.25M" 110 | }, 111 | { 112 | "name": "Memory: Swap Usage [End]", 113 | "value": "8550.75M" 114 | }, 115 | { 116 | "name": "Memory: Total", 117 | "value": "16G" 118 | }, 119 | { 120 | "name": "Modules Count", 121 | "value": "414" 122 | }, 123 | { 124 | "name": "OS: Build", 125 | "value": "21C52" 126 | }, 127 | { 128 | "name": "OS: Name", 129 | "value": "macOS" 130 | }, 131 | { 132 | "name": "OS: Version", 133 | "value": "12.1" 134 | }, 135 | { 136 | "name": "Power: Source", 137 | "value": "AC Power" 138 | }, 139 | { 140 | "name": "Version: AGP" 141 | }, 142 | { 143 | "name": "Version: JDK", 144 | "value": "11" 145 | }, 146 | { 147 | "name": "Version: Kotlin" 148 | } 149 | ], 150 | "links": [ 151 | { 152 | "label": "Git commit id build scans", 153 | "url": "https://ge.gradle.org/scans?search.names=Git+commit+id+short&search.values=d92128a2#selection.buildScanB=psa5uc3o2hebc" 154 | } 155 | ], 156 | "gradleEnterpriseSettings": { 157 | "backgroundPublicationEnabled": true, 158 | "buildOutputCapturingEnabled": true, 159 | "taskInputsFileCapturingEnabled": true, 160 | "testOutputCapturingEnabled": true 161 | }, 162 | "develocitySettings": { 163 | "backgroundPublicationEnabled": true, 164 | "buildOutputCapturingEnabled": true, 165 | "taskInputsFileCapturingEnabled": true, 166 | "testOutputCapturingEnabled": true 167 | }, 168 | "buildOptions": { 169 | "buildCacheEnabled": true, 170 | "configurationCacheEnabled": false, 171 | "configurationOnDemandEnabled": true, 172 | "continuousBuildEnabled": false, 173 | "continueOnFailureEnabled": false, 174 | "daemonEnabled": true, 175 | "dryRunEnabled": false, 176 | "excludedTasks": [], 177 | "fileSystemWatchingEnabled": true, 178 | "maxNumberOfGradleWorkers": 12, 179 | "offlineModeEnabled": false, 180 | "parallelProjectExecutionEnabled": true, 181 | "refreshDependenciesEnabled": false, 182 | "rerunTasksEnabled": false 183 | }, 184 | "environment": { 185 | "username": "example", 186 | "operatingSystem": "macOS 12.1 (x86_64)", 187 | "numberOfCpuCores": 12, 188 | "jreVersion": "Oracle OpenJDK Runtime Environment 11.0.12+8-LTS-237", 189 | "jvmVersion": "Oracle Java HotSpot(TM) 64-Bit Server VM 11.0.12+8-LTS-237 (mixed mode)", 190 | "jvmMaxMemoryHeapSize": 5368709120, 191 | "jvmCharset": "UTF-8", 192 | "jvmLocale": "English (Brazil)", 193 | "publicHostname": "13654-MB" 194 | } 195 | } -------------------------------------------------------------------------------- /library/src/testFixtures/kotlin/com/gabrielfeo/develocity/api/FakeDevelocityApiScaffold.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import com.gabrielfeo.develocity.api.model.* 4 | import retrofit2.http.Query 5 | 6 | /** 7 | * Scaffold for a fake `DevelocityApi` implementation with default methods throwing a 8 | * [NotImplementedError]. Extend this interface and override methods to fake behavior as needed. 9 | */ 10 | interface FakeBuildsApiScaffold : BuildsApi { 11 | 12 | override suspend fun getBuild( 13 | id: String, 14 | models: List?, 15 | allModels: Boolean?, 16 | availabilityWaitTimeoutSecs: Int?, 17 | ): Build { 18 | TODO("Not yet implemented") 19 | } 20 | 21 | override suspend fun getBazelAttributes( 22 | id: String, 23 | availabilityWaitTimeoutSecs: Int? 24 | ): BazelAttributes { 25 | TODO("Not yet implemented") 26 | } 27 | 28 | override suspend fun getBazelCriticalPath( 29 | id: String, 30 | availabilityWaitTimeoutSecs: Int? 31 | ): BazelCriticalPath { 32 | TODO("Not yet implemented") 33 | } 34 | 35 | override suspend fun getBuilds( 36 | fromInstant: Long?, 37 | fromBuild: String?, 38 | reverse: Boolean?, 39 | maxBuilds: Int?, 40 | maxWaitSecs: Int?, 41 | query: String?, 42 | models: List?, 43 | allModels: Boolean?, 44 | since: Long?, 45 | sinceBuild: String?, 46 | ): List { 47 | TODO("Not yet implemented") 48 | } 49 | 50 | override suspend fun getGradleBuildProfileOverview( 51 | id: String, 52 | availabilityWaitTimeoutSecs: Int? 53 | ): GradleBuildProfileOverview { 54 | TODO("Not yet implemented") 55 | } 56 | 57 | override suspend fun getGradleConfigurationCache( 58 | id: String, 59 | availabilityWaitTimeoutSecs: Int? 60 | ): GradleConfigurationCache { 61 | TODO("Not yet implemented") 62 | } 63 | 64 | override suspend fun getGradlePlugins( 65 | id: String, 66 | availabilityWaitTimeoutSecs: Int? 67 | ): GradlePlugins { 68 | TODO("Not yet implemented") 69 | } 70 | 71 | override suspend fun getGradleResourceUsage( 72 | id: String, 73 | availabilityWaitTimeoutSecs: Int? 74 | ): GradleResourceUsage { 75 | TODO("Not yet implemented") 76 | } 77 | 78 | override suspend fun getMavenBuildProfileOverview( 79 | id: String, 80 | availabilityWaitTimeoutSecs: Int? 81 | ): MavenBuildProfileOverview { 82 | TODO("Not yet implemented") 83 | } 84 | 85 | override suspend fun getMavenPlugins( 86 | id: String, 87 | availabilityWaitTimeoutSecs: Int? 88 | ): MavenPlugins { 89 | TODO("Not yet implemented") 90 | } 91 | 92 | override suspend fun getMavenResourceUsage( 93 | id: String, 94 | availabilityWaitTimeoutSecs: Int? 95 | ): MavenResourceUsage { 96 | TODO("Not yet implemented") 97 | } 98 | 99 | override suspend fun getGradleArtifactTransformExecutions( 100 | id: String, 101 | availabilityWaitTimeoutSecs: Int?, 102 | ): GradleArtifactTransformExecutions { 103 | TODO("Not yet implemented") 104 | } 105 | 106 | override suspend fun getGradleAttributes(id: String, availabilityWaitTimeoutSecs: Int?): GradleAttributes { 107 | TODO("Not yet implemented") 108 | } 109 | 110 | override suspend fun getGradleBuildCachePerformance( 111 | id: String, 112 | availabilityWaitTimeoutSecs: Int?, 113 | ): GradleBuildCachePerformance { 114 | TODO("Not yet implemented") 115 | } 116 | 117 | override suspend fun getGradleDeprecations(id: String, availabilityWaitTimeoutSecs: Int?): GradleDeprecations { 118 | TODO("Not yet implemented") 119 | } 120 | 121 | override suspend fun getGradleNetworkActivity( 122 | id: String, 123 | availabilityWaitTimeoutSecs: Int?, 124 | ): GradleNetworkActivity { 125 | TODO("Not yet implemented") 126 | } 127 | 128 | override suspend fun getGradleProjects(id: String, availabilityWaitTimeoutSecs: Int?): List { 129 | TODO("Not yet implemented") 130 | } 131 | 132 | override suspend fun getMavenAttributes(id: String, availabilityWaitTimeoutSecs: Int?): MavenAttributes { 133 | TODO("Not yet implemented") 134 | } 135 | 136 | override suspend fun getMavenBuildCachePerformance( 137 | id: String, 138 | availabilityWaitTimeoutSecs: Int?, 139 | ): MavenBuildCachePerformance { 140 | TODO("Not yet implemented") 141 | } 142 | 143 | override suspend fun getMavenDependencyResolution( 144 | id: String, 145 | availabilityWaitTimeoutSecs: Int?, 146 | ): MavenDependencyResolution { 147 | TODO("Not yet implemented") 148 | } 149 | 150 | override suspend fun getMavenModules(id: String, availabilityWaitTimeoutSecs: Int?): List { 151 | TODO("Not yet implemented") 152 | } 153 | 154 | override suspend fun getGradleTestPerformance( 155 | id: String, 156 | availabilityWaitTimeoutSecs: Int? 157 | ): GradleTestPerformance { 158 | TODO("Not yet implemented") 159 | } 160 | 161 | override suspend fun getMavenTestPerformance(id: String, availabilityWaitTimeoutSecs: Int?): MavenTestPerformance { 162 | TODO("Not yet implemented") 163 | } 164 | 165 | override suspend fun getNpmAttributes(id: String, availabilityWaitTimeoutSecs: Int?): NpmAttributes { 166 | TODO("Not yet implemented") 167 | } 168 | 169 | override suspend fun getPythonAttributes(id: String, availabilityWaitTimeoutSecs: Int?): PythonAttributes { 170 | TODO("Not yet implemented") 171 | } 172 | 173 | override suspend fun getGradleDependencies(id: String, availabilityWaitTimeoutSecs: Int?): GradleDependencies { 174 | TODO("Not yet implemented") 175 | } 176 | 177 | override suspend fun getMavenDependencies(id: String, availabilityWaitTimeoutSecs: Int?): MavenDependencies { 178 | TODO("Not yet implemented") 179 | } 180 | 181 | override suspend fun getSbtAttributes(id: String, availabilityWaitTimeoutSecs: Int?): SbtAttributes { 182 | TODO("Not yet implemented") 183 | } 184 | 185 | override suspend fun getMavenExtensions(id: String, availabilityWaitTimeoutSecs: Int?): MavenExtensions { 186 | TODO("Not yet implemented") 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /library/src/testFixtures/kotlin/com/gabrielfeo/develocity/api/Resources.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api 2 | 3 | import java.net.URL 4 | import java.nio.file.Path 5 | import kotlin.io.path.ExperimentalPathApi 6 | import kotlin.io.path.copyToRecursively 7 | import kotlin.io.path.createParentDirectories 8 | import kotlin.io.path.div 9 | 10 | 11 | @OptIn(ExperimentalPathApi::class) 12 | fun Any.copyFromResources(path: String, targetDir: Path) { 13 | val sourcePath = Path.of(requireResource(path).toURI()) 14 | val destPath = targetDir / path.removePrefix("/") 15 | destPath.createParentDirectories() 16 | sourcePath.copyToRecursively(destPath, followLinks = false, overwrite = true) 17 | } 18 | 19 | fun Any.requireResource(path: String): URL = 20 | requireNotNull(this::class.java.getResource(path)) 21 | -------------------------------------------------------------------------------- /library/src/testFixtures/kotlin/com/gabrielfeo/develocity/api/internal/FakeEnv.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal 2 | 3 | class FakeEnv( 4 | vararg vars: Pair, 5 | ) : Env { 6 | 7 | private val vars = vars.toMap(HashMap()) 8 | 9 | override fun get(name: String) = vars[name] 10 | 11 | operator fun set(name: String, value: String?) = vars.put(name, value) 12 | operator fun contains(name: String) = name in vars 13 | } 14 | -------------------------------------------------------------------------------- /library/src/testFixtures/kotlin/com/gabrielfeo/develocity/api/internal/FakeSystemProperties.kt: -------------------------------------------------------------------------------- 1 | package com.gabrielfeo.develocity.api.internal 2 | 3 | data class FakeSystemProperties( 4 | override var userHome: String? = System.getProperty("java.io.tmpdir"), 5 | ) : SystemProperties 6 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | includeBuild("./build-logic") 3 | } 4 | 5 | plugins { 6 | id("com.gradle.develocity") version("4.2.1") 7 | id("com.gradle.common-custom-user-data-gradle-plugin") version("2.4.0") 8 | id("org.gradle.toolchains.foojay-resolver-convention") version("1.0.0") 9 | } 10 | 11 | include( 12 | ":library", 13 | ) 14 | 15 | dependencyResolutionManagement { 16 | repositories { 17 | mavenCentral() 18 | } 19 | } 20 | 21 | develocity { 22 | buildScan { 23 | termsOfUseUrl = "https://gradle.com/help/legal-terms-of-use" 24 | termsOfUseAgree = "yes" 25 | obfuscation { 26 | ipAddresses { addresses -> addresses.map { _ -> "0.0.0.0" } } 27 | hostname { "-redacted-" } 28 | } 29 | } 30 | } 31 | --------------------------------------------------------------------------------