├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── bug_report.md │ ├── feature.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── maven-branch-ci.yml │ ├── maven-master-cicd.yml │ └── settings.xml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── examples ├── README.md ├── helper-script │ ├── .gitignore │ ├── Deployer.groovy │ ├── Dockerfile │ ├── Jenkinsfile │ ├── Makefile │ ├── README.md │ ├── app │ │ ├── counter.py │ │ ├── hello.py │ │ ├── test_counter.py │ │ └── test_hello.py │ ├── pom.xml │ └── src │ │ └── test │ │ ├── groovy │ │ ├── DeployerSpec.groovy │ │ └── JenkinsfileSpec.groovy │ │ └── resources │ │ └── logback-test.xml ├── shared-library │ ├── Makefile │ ├── README.md │ ├── pom.xml │ ├── resources │ │ └── com │ │ │ └── example │ │ │ └── SlackMessageTemplate.txt │ ├── src │ │ └── com │ │ │ └── example │ │ │ └── SharedLibraryConstants.groovy │ ├── test │ │ ├── DefaultPipelineSpec.groovy │ │ ├── DeployerSpec.groovy │ │ └── resources │ │ │ └── logback-test.xml │ └── vars │ │ ├── DefaultPipeline.groovy │ │ └── Deployer.groovy └── whole-pipeline │ ├── Dockerfile │ ├── Jenkinsfile │ ├── Makefile │ ├── README.md │ ├── app │ ├── counter.py │ ├── hello.py │ ├── test_counter.py │ └── test_hello.py │ ├── pom.xml │ └── src │ └── test │ ├── groovy │ └── JenkinsfileSpec.groovy │ └── resources │ └── logback-test.xml ├── pom.xml └── src ├── main ├── assembly │ └── javadoc.xml ├── groovy │ └── com │ │ └── homeaway │ │ └── devtools │ │ └── jenkins │ │ └── testing │ │ ├── InvalidlyNamedScriptWrapper.groovy │ │ ├── JenkinsPipelineSpecification.groovy │ │ └── PipelineVariableImpersonator.groovy └── java │ └── com │ └── homeaway │ └── devtools │ └── jenkins │ └── testing │ ├── APipelineExtensionDetector.java │ ├── LocalProjectPipelineExtensionDetector.java │ └── WholeClasspathPipelineExtensionDetector.java └── test ├── groovy └── com │ └── homeaway │ └── devtools │ └── jenkins │ └── testing │ ├── ArgumentCaptureSpec.groovy │ ├── DescriptorTimeJenkinsAccessingStepSpec.groovy │ ├── DescriptorTimeJenkinsInteractingSpec.groovy │ ├── ParallelClosureExecutionSpec.groovy │ ├── TrailngClosureExecutionSpec.groovy │ ├── functions │ ├── ClassToTest.groovy │ ├── ClassToTestSpec.groovy │ ├── ClassWithMissingHandlers.groovy │ ├── ClassWithMissingHandlersSpec.groovy │ ├── ClassWithNoMethods.groovy │ └── ClassWithNoMethodsSpec.groovy │ ├── jobs │ └── JenkinsfileSpec.groovy │ ├── properties │ ├── JenkinsfileWithExplicitlyDefinedPropertySpec.groovy │ ├── JenkinsfileWithPropertySpec.groovy │ └── JenkinsfileWithUndefinedPropertySpec.groovy │ └── scripts │ └── InvalidClassNameSpec.groovy ├── java └── com │ └── homeaway │ └── devtools │ └── jenkins │ └── testing │ └── DescriptorTimeJenkinsAccessingStep.java └── resources ├── com └── homeaway │ └── devtools │ └── jenkins │ └── testing │ ├── jobs │ └── Jenkinsfile.groovy │ ├── properties │ ├── JenkinsfileWithExplicitlyDefinedProperty.groovy │ ├── JenkinsfileWithProperty.groovy │ └── JenkinsfileWithUndefinedProperty.groovy │ └── scripts │ ├── some-script.groovy │ └── some_script.groovy └── logback-test.xml /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | Bug Report 2 | ============================== 3 | 4 | Expected Behavior 5 | ============================== 6 | 7 | Please describe the expected behavior of the project. 8 | 9 | Actual Behavior 10 | ============================== 11 | 12 | Please describe what the project is doing _instead_ of the expected behavior 13 | 14 | Steps to Reproduce 15 | ============================== 16 | 17 | Please include a numbered list of steps that other people can use to recreate the "Actual Behavior." 18 | 19 | 1. First Step 20 | 2. Second Step 21 | 3. Third Step 22 | 23 | Additional Information 24 | ============================== 25 | 26 | Provide any additional information that might be helpful, such as steps you've tried to resolve the issue, or observations you've made about the bug. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | Expected Behavior 8 | ============================== 9 | 10 | Please describe the expected behavior of the project. 11 | 12 | Actual Behavior 13 | ============================== 14 | 15 | Please describe what the project is doing _instead_ of the expected behavior 16 | 17 | Steps to Reproduce 18 | ============================== 19 | 20 | Please include a numbered list of steps that other people can use to recreate the "Actual Behavior." 21 | 22 | 1. First Step 23 | 2. Second Step 24 | 3. Third Step 25 | 26 | Additional Information 27 | ============================== 28 | 29 | Provide any additional information that might be helpful, such as steps you've tried to resolve the issue, or observations you've made about the bug. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | Feature Request 2 | ============================== 3 | 4 | Desired Behavior 5 | ============================== 6 | 7 | Please describe the new behavior the project should have. 8 | 9 | Benefits 10 | ============================== 11 | 12 | Please list the benefits of updating the project to have the new behavior, e.g. 13 | 14 | 1. Builds more quickly 15 | 2. Enable compatibility with a new platform 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | Desired Behavior 8 | ============================== 9 | 10 | Please describe the new behavior the project should have. 11 | 12 | Benefits 13 | ============================== 14 | 15 | Please list the benefits of updating the project to have the new behavior, e.g. 16 | 17 | 1. Builds more quickly 18 | 2. Enable compatibility with a new platform 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Summary 2 | ============================== 3 | 4 | Please briefly describe how this Pull Request changes the project. 5 | 6 | Additional Details 7 | ============================== 8 | 9 | If necessary, provide additional context or details about this Pull Request. 10 | 11 | Checklist 12 | ============================== 13 | 14 | Testing 15 | ------------------------- 16 | 17 | (Remove this checklist and replace it with "N/A - no code changes" if this PR does not modify source code) 18 | 19 | * [ ] I have manually verified that my code changes do the right thing. 20 | * [ ] I have run the tests and verified that my changes do not introduce any regressions. 21 | * [ ] I have written unit tests to verify that my code changes do the right thing and to protect my code against regressions 22 | 23 | Documentation 24 | ------------------------- 25 | 26 | (Remove this checklist and replace it with "N/A - no code changes" if this PR does not modify source code) 27 | 28 | * [ ] I have updated the "Unreleased" section of `CHANGELOG.md` with a brief description of my changes. 29 | * [ ] I have updated code comments - both GroovyDoc/JavaDoc-style comments and inline comments - where appropriate. 30 | * [ ] I have read `CONTRIBUTING.md` and have followed its guidance. 31 | -------------------------------------------------------------------------------- /.github/workflows/maven-branch-ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | "master" 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Set up JDK 1.8 16 | uses: actions/setup-java@v1 17 | with: 18 | java-version: 1.8 19 | - name: Download Dependencies 20 | run: mvn --batch-mode dependency:go-offline -Pit 21 | - name: Build & Test with Maven 22 | run: mvn verify 23 | --batch-mode 24 | -P it 25 | -D gpg.skip 26 | -------------------------------------------------------------------------------- /.github/workflows/maven-master-cicd.yml: -------------------------------------------------------------------------------- 1 | name: Master Test & Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | "master" 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Set up GPG 16 | run: gpg --version && 17 | echo "${{ secrets.GPG_KEY_BASE64 }}" | base64 --decode | gpg --batch --import && 18 | echo "trusted-key 0xF0A00ADA5404ED0A" >> ~/.gnupg/gpg.conf && 19 | echo "default-key 0xF0A00ADA5404ED0A" >> ~/.gnupg/gpg.conf && 20 | gpg --list-secret-keys 21 | - name: Set up JDK 1.8 22 | uses: actions/setup-java@v1 23 | with: 24 | java-version: 1.8 25 | - name: Download Dependencies 26 | run: mvn --batch-mode dependency:go-offline -Pit 27 | - name: Build & Test with Maven 28 | run: mvn deploy 29 | --batch-mode 30 | --settings .github/workflows/settings.xml 31 | -P it 32 | -P noninteractive-gpg 33 | -D gpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" 34 | -D repo.id=ossrh 35 | -D repo.username="${{ secrets.MAVEN_CENTRAL_USERNAME }}" 36 | -D repo.password="${{ secrets.MAVEN_CENTRAL_PASSWORD }}" 37 | -------------------------------------------------------------------------------- /.github/workflows/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ${repo.id} 9 | ${repo.username} 10 | ${repo.password} 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .classpath 3 | .project 4 | .settings 5 | .checkstyle 6 | *.ipr 7 | *.iml 8 | *.iws 9 | .idea 10 | *.pyc 11 | build.log 12 | **/*.versionsBackup 13 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ============================== 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | * Scan classpaths for extensions only once 11 | * Fixed a bug for Java 11 compatibility 12 | 13 | ## 2.1.5 14 | 15 | _Release Date: 2021-04-13_ 16 | 17 | ### Updated 18 | 19 | * the deprecated `fast-classpath-scanner` dependency to its latest form `classgraph`. 20 | * the deprecated `FastClasspathScanner` class to its latest form `ClassGraph`. 21 | 22 | ## 2.1.4 23 | 24 | _Release Date: 2020-06-01_ 25 | 26 | ### Added 27 | 28 | * Solution for Jenkins extensions that interact with the `Jenkins` singelton at classload- or Descriptor-instantiation-time: 29 | * jenkins-spock automatically injects a mock Jenkins singleton _before_ any Jenkins extensions are classloaded or instantiated, so that `Jenkins.getInstanceOrNull()` is not `null`. 30 | * `makeStaticJenkins()` method to allow test suites to provide their own pre-test-suite Jenkins singleton if necessary, such as if the spec needs to stub pre-test-suite interaction with `Jenkins` 31 | * Please see the "Mock Jenkins" section of the `JenkinsPipelineSpecification` GroovyDoc. 32 | 33 | ## 2.1.3 34 | 35 | _Release Date: 2020-05-29_ 36 | 37 | ### Updated 38 | 39 | * `jenkins-spock` version used in examples is updated to the released version on release. 40 | 41 | ## 2.1.2 42 | 43 | _Release Date: 2020-03-12_ 44 | 45 | ### Updated 46 | 47 | * Updated Reflections to 0.9.12 48 | 49 | ## 2.1.1 50 | 51 | _Release Date: 2020-02-06_ 52 | 53 | ### Fixed 54 | 55 | * Could not use `loadPipelineScriptForTest(...)` with Groovy scripts whose filenames were not valid Java class names. 56 | 57 | ## 2.1.0 58 | 59 | _Release Date: 2020-02-04_ 60 | 61 | ### Fixed 62 | 63 | * Added `` section to all poms pointing to the Jenkins Release repository - so that Jenkins artifacts can be successfully located during builds. 64 | 65 | ### Updated 66 | 67 | * Emit more-specific exception when tests call a Groovy method with an incorrect signature. 68 | * Clarified documentation for using "global" variables in tests. 69 | 70 | ### Added 71 | 72 | * Make the resource path lookup customizable for `JenkinsPipelineSpecification#loadPipelineScriptForTest`. To define your own path set `JenkinsPipelineSpecification.scriptClassPath`. 73 | * The `maven-invoker-plugin` now runs some of the "working example" projects as integration tests. 74 | * The `parallel()` pipeline step is special-cased and will execute all of its closures during tests. 75 | 76 | ## 2.0.1 77 | 78 | _Release Date: 2018-09-13_ 79 | 80 | ### Fixed 81 | 82 | * Layout of documentation files in `-javadoc` artifact was incorrect. 83 | 84 | ## 2.0.0 85 | 86 | _Release Date: 2018-09-12_ 87 | 88 | Initial OSS Release. 89 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contribution Guide 2 | ============================== 3 | 4 | Bug Reports & Feature Requests 5 | ------------------------- 6 | 7 | If you have a bug report or feature request that is not accompanied by a Pull Request, please create a GitHub Issue. 8 | 9 | Code Changes 10 | ------------------------- 11 | 12 | To submit code changes, please fork this repository and send a pull request to the `master` branch. 13 | 14 | ### Code Style 15 | 16 | Please make an effort to match the style present in the particular files that you edit. 17 | 18 | ### Testing 19 | 20 | If code changes are testable, the pull request should include new test code. 21 | 22 | ### Documentation 23 | 24 | Any code change that changes the behavior of the project should be accompanied by appropriate documentation updates. 25 | This includes updating the "unreleased" section of the [`CHANGELOG`](CHANGELOG.md). 26 | 27 | Documentation Changes 28 | ------------------------- 29 | 30 | _(such as to README and other `*.md` files)_ 31 | 32 | To submit changes to _documentation_ (JavaDoc/GroovyDoc changes are _code changes_ as these changes are made in source code files), please fork this repository and send a pull request to the `master` branch. 33 | 34 | Project maintainers may make documentation-only commits directly to the `master` branch without going through the Pull Request process. 35 | 36 | Documentation-only commits do not require an update to the [`CHANGELOG`](CHANGELOG.md). 37 | 38 | CI/CD 39 | ------------------------- 40 | 41 | There is currently no CI/CD system connected. HomeAway's maintainers will manually verify that pull request code can build & passes its tests. 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright (c) 2018 HomeAway, Inc. 179 | All rights reserved. http://www.homeaway.com 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Jenkins Pipeline Support for Spock 2 | ============================== 3 | 4 | Utility classes to help with testing Jenkins pipeline scripts and functions written in Groovy. 5 | 6 | User Guide _([GroovyDoc](https://javadoc.io/doc/com.homeaway.devtools.jenkins/jenkins-spock))_ 7 | ============================== 8 | 9 | Add this library to `pom.xml` in the `test` scope: 10 | 11 | ```xml 12 | 13 | com.homeaway.devtools.jenkins 14 | jenkins-spock 15 | test 16 | 17 | ``` 18 | 19 | Check the [CHANGELOG.md](CHANGELOG.md) to find details about the available versions. 20 | 21 | Working Examples 22 | ------------------------- 23 | 24 | The [`examples` directory](examples) contains working sample projects that show off the major kinds of project this library can be used with. 25 | Check them out and try building them yourself! 26 | 27 | Specifications 28 | ------------------------- 29 | 30 | This library provides a `JenkinsPipelineSpecification` class that extends the Spock testing framework's `Specification` class. To test Jenkins pipeline Groovy code, extend `JenkinsPipelineSpecification` instead of `Specification`. 31 | Please see the [GroovyDoc for `JenkinsPipelineSpecification`](http://javadoc.io/page/com.homeaway.devtools.jenkins/jenkins-spock/latest/com/homeaway/devtools/jenkins/testing/JenkinsPipelineSpecification.html) for specific usage information and the [Spock Framework Documentation](http://docs.spockframework.org/) for general usage information. 32 | 33 | During the tests of a `JenkinsPipelineSpecification` suite, 34 | 35 | 1. All Jenkins pipeline steps (`@StepDescriptor`s) will be globally callable, e.g. you can just write `sh( "echo hello" )` anywhere. 36 | 1. "Body" closure blocks passed to any mock pipeline steps will be executed. 37 | 2. All Jenkins pipeline variables (`@Symbol`s and `GlobalVariable`s) will be globally accessible, e.g. you can just write `docker.inside(...)` anywhere 38 | 3. All Pipeline Shared Library Global Variables (from the `/vars` directory) will be globally accessible, so you can just use them anywhere. 39 | 4. All interactions with any of those pipeline extension points will be captured by Spock mock objects. 40 | 5. You can load any Groovy script (`Jenkinsfile` or Shared Library variable) to unit-test it in isolation. 41 | 6. The `Jenkins` singleton instance will exist as a Spock mock object. 42 | 7. The `CpsScript` execution will exist as a Spock spy object (you should never need to interact with this, but it's there). 43 | 44 | Dependencies 45 | ------------------------- 46 | 47 | There are some dependencies of this library that are marked with Maven's `provided` scope. 48 | This means that Maven will pull them in for building and testing _this library_, but when you use this library you must pull those libraries in as dependencies yourself. 49 | 50 | This is done because these dependencies - things like the Jenkins Pipeline API, JUnit, etc - are things that 51 | 52 | 1. You absolutely have to have as dependencies in your project in order for this library to be of any use 53 | 2. Should not have their version or final scope controlled by this library 54 | 55 | The dependencies that should already be in your project in order for using this library to make any sense are: 56 | 57 | ```xml 58 | 59 | org.jenkins-ci.main 60 | jenkins-core 61 | ${jenkins.version} 62 | test 63 | 64 | 65 | org.jenkins-ci.plugins.workflow 66 | workflow-step-api 67 | ${jenkins.workflow.step.version} 68 | test 69 | 70 | 71 | org.jenkins-ci.plugins.workflow 72 | workflow-cps 73 | ${jenkins.workflow.cps.version} 74 | test 75 | 76 | 77 | org.jenkins-ci 78 | symbol-annotation 79 | ${jenkins.symbol.version} 80 | test 81 | 82 | 83 | 84 | junit 85 | junit 86 | ${junit.version} 87 | test 88 | 89 | 90 | 91 | javax.servlet 92 | javax.servlet-api 93 | ${jenkins.servlet.version} 94 | test 95 | 96 | ``` 97 | 98 | Depending on your parent pom, some of the `${jenkins.version}` properties may already be defined. Be sure you define any that are not. 99 | 100 | If your code actually writes code against classes in any of these dependencies, remove the `test` entry for the corresponding block(s). 101 | 102 | Developer Guide 103 | ============================== 104 | 105 | Building 106 | ------------------------- 107 | 108 | The build of **jenkins-spock** is built with Maven. Normal [Maven lifecycle phases](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html) apply. 109 | As long as you have a contemporary (1.8+) JDK and Maven (3.3+), it should build fine. 110 | 111 | Testing 112 | ------------------------- 113 | 114 | Unit tests of `jenkins-spock` will happen automatically during the `test` phase of the Maven build. 115 | 116 | There is an `it` [Maven Profile](https://maven.apache.org/guides/introduction/introduction-to-profiles.html) that can be activated to run _integration tests_: 117 | 118 | mvn verify -Pit 119 | 120 | The integration tests will run `mvn verify` on some of the [Working Example Projects](examples), using the current `jenkins-spock` code. 121 | 122 | Releasing 123 | ------------------------- 124 | 125 | **jenkins-spock** should be released by the [`maven-release-plugin`](https://maven.apache.org/maven-release/maven-release-plugin/): 126 | 127 | mvn clean release:prepare release:perform 128 | 129 | In order for this to succeed, the user running this must 130 | 131 | 1. Configure GitHub credentials with `push` access to this repository. 132 | 2. Configure Sonatype Nexus credentials with deploy access to the `com.homeaway` groupId. 133 | 3. Configure a PGP identity so that the [`maven-gpg-plugin`](https://maven.apache.org/plugins/maven-gpg-plugin/) can sign artifacts. 134 | 1. Locally, run GPG 2.1 or newer 135 | 2. Set the default signing key to the key you want to use 136 | 3. Provide -Dgpg.passphrase on the command-line _or_ run interactively to be able to enter a passphrase -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Jenkins + Spock Examples 2 | ============================== 3 | 4 | Here are some example projects that use Jenkins Pipeline code in different ways and show off how to write unit tests for that code using `jenkins-spock`. 5 | 6 | These example repositories work as much as possible, but their Jenkins pipelines and "deploy application" logic are made-up and won't actually run or deploy anything to anywhere. 7 | 8 | Integration Tests 9 | ------------------------- 10 | 11 | Some of these example projects are actually run as integration tests during the build of `jenkins-spock`. 12 | 13 | Check the `fixup-integration-test-projects` execution of the `maven-invoker-plugin` in the `it` Maven profile in the `pom.xml` to see which examples are used as integration tests. -------------------------------------------------------------------------------- /examples/helper-script/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /examples/helper-script/Deployer.groovy: -------------------------------------------------------------------------------- 1 | def deploy( _env ) { 2 | 3 | def DEPLOY_COMMAND=""" 4 | docker-compose pull && \ 5 | docker-compose down && \ 6 | docker-compose rm -f && \ 7 | docker-compose up -d --force-recreate""" 8 | 9 | if( _env == "test" ) { 10 | sshagent(["test-ssh"]) { 11 | sh( "ssh deployer@app-test -c '${DEPLOY_COMMAND}'" ) 12 | } 13 | } else if( _env == "production" ) { 14 | sshagent(["prod-ssh"]) { 15 | sh( "ssh deployer@app-prod -c '${DEPLOY_COMMAND}'" ) 16 | } 17 | } 18 | } 19 | 20 | return this 21 | -------------------------------------------------------------------------------- /examples/helper-script/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jfloff/alpine-python:3.6 2 | 3 | RUN pip install --upgrade \ 4 | Flask \ 5 | pip 6 | 7 | COPY app /app 8 | 9 | WORKDIR app 10 | ENV FLASK_APP=hello.py 11 | EXPOSE 5000 12 | 13 | ENTRYPOINT ["flask", "run", "--host", "0.0.0.0"] 14 | -------------------------------------------------------------------------------- /examples/helper-script/Jenkinsfile: -------------------------------------------------------------------------------- 1 | def Deployer = null 2 | 3 | node { 4 | stage( "Checkout" ) { 5 | checkout scm 6 | Deployer = load( "Deployer.groovy" ) 7 | } 8 | 9 | stage( "Build" ) { 10 | sh( "docker build --tag whole-pipeline ." ) 11 | } 12 | 13 | stage( "Test" ) { 14 | try { 15 | sh( "docker run --entrypoint python whole-pipeline -m unittest discover" ) 16 | } catch( Exception e ) { 17 | slackSend( 18 | color: 'error', 19 | message: 'whole-pipeline unit tests failed.' ) 20 | throw e 21 | } 22 | } 23 | 24 | stage( "Push" ) { 25 | sh( "docker push whole-pipeline" ) 26 | } 27 | 28 | stage( "Deploy to TEST" ) { 29 | Deployer.deploy( "test" ) 30 | } 31 | 32 | if( BRANCH_NAME == "master" ) { 33 | stage( "Deploy to PRODUCTION" ) { 34 | Deployer.deploy( "production" ) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/helper-script/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: 3 | $(MAKE) test 4 | $(MAKE) test-pipeline 5 | 6 | .PHONY: build 7 | build: 8 | docker build -t helper-script . 9 | 10 | .PHONY: run 11 | run: build 12 | docker run --rm -it --publish 5000:5000 helper-script 13 | 14 | .PHONY: test 15 | test: build 16 | docker run --entrypoint python helper-script -m unittest discover 17 | 18 | .PHONY: test-pipeline 19 | test-pipeline: 20 | mvn clean verify 21 | -------------------------------------------------------------------------------- /examples/helper-script/README.md: -------------------------------------------------------------------------------- 1 | `Jenkinsfile` + Helper Script 2 | ============================== 3 | 4 | Sometimes you break big pieces of code out into separate files, and `load(...)` them in your `Jenkinsfile`. 5 | 6 | This project is a containerized Python web application with a Jenkins pipeline that 7 | 8 | 1. Builds & Tests all branches 9 | 2. Notifies Slack if tests fail 10 | 3. Deploys the "master" branch to the Production environment 11 | 4. Deploys other branches to the "Test" environment. 12 | 13 | Building 14 | ============================== 15 | 16 | To build the web application, run `make build`. 17 | 18 | Running 19 | ============================== 20 | 21 | To run the application, run `make run`. 22 | 23 | To see the running application, visit http://localhost:5000 in a web browser. 24 | 25 | Testing 26 | ============================== 27 | 28 | To test the _application_, run `make test`. 29 | 30 | To test the _pipeline_, run `make test-pipeline`. 31 | 32 | Requirements 33 | ============================== 34 | 35 | The following tools should be installed in order to work with this project: 36 | 37 | 1. `docker` 38 | 2. `make` 39 | 3. `java` 1.8+ 40 | 4. `maven` 3.3+ 41 | -------------------------------------------------------------------------------- /examples/helper-script/app/counter.py: -------------------------------------------------------------------------------- 1 | def plusone(_number): 2 | return _number + 1 3 | -------------------------------------------------------------------------------- /examples/helper-script/app/hello.py: -------------------------------------------------------------------------------- 1 | import counter 2 | from flask import Flask 3 | 4 | app = Flask(__name__) 5 | greeted_times = 0 6 | 7 | @app.route("/") 8 | def hello(): 9 | global greeted_times 10 | 11 | greeting = "Hello World! I've greeted {0} times!".format( greeted_times ) 12 | 13 | greeted_times = counter.plusone( greeted_times ) 14 | 15 | return greeting -------------------------------------------------------------------------------- /examples/helper-script/app/test_counter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import counter 3 | 4 | class TestCounter(unittest.TestCase): 5 | def test_plusone(self): 6 | self.assertEqual(2, counter.plusone(1)) -------------------------------------------------------------------------------- /examples/helper-script/app/test_hello.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import hello 3 | 4 | class TestCounter(unittest.TestCase): 5 | def test_hello(self): 6 | self.assertEqual( "Hello World! I've greeted 0 times!", hello.hello() ) 7 | self.assertEqual( "Hello World! I've greeted 1 times!", hello.hello() ) 8 | self.assertEqual( "Hello World! I've greeted 2 times!", hello.hello() ) -------------------------------------------------------------------------------- /examples/helper-script/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | com.example 8 | jenkinsfile-test-helper-script 9 | jar 10 | 0.0.0-SNAPSHOT 11 | 12 | Testing a Jenkinsfile and Helper Script with Spock 13 | 14 | 15 | 16 | jenkins-releases 17 | Jenkins Releases 18 | http://repo.jenkins-ci.org/releases 19 | 20 | 21 | jenkins-public 22 | Jenkins Public 23 | http://repo.jenkins-ci.org/public 24 | 25 | 26 | 27 | 28 | 29 | 2.4.11 30 | 1.1-groovy-2.4 31 | 1.6.1 32 | 33 | 2.102 34 | 3.1.0 35 | 1.10 36 | 2.36 37 | 2.10 38 | 39 | 2.1.5 40 | 41 | 4.12 42 | 43 | logback-test.xml 44 | ${project.build.directory}/log 45 | ERROR 46 | 1.2.3 47 | 1.7.25 48 | 49 | 2.22.0 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | com.google.guava 58 | guava 59 | 20.0 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | com.homeaway.devtools.jenkins 68 | jenkins-spock 69 | ${jenkins-spock.version} 70 | test 71 | 72 | 73 | 74 | ch.qos.logback 75 | logback-core 76 | ${log.logback.version} 77 | runtime 78 | 79 | 80 | ch.qos.logback 81 | logback-classic 82 | ${log.logback.version} 83 | runtime 84 | 85 | 86 | 87 | javax.servlet 88 | javax.servlet-api 89 | ${jenkins.servlet.version} 90 | test 91 | 92 | 93 | 94 | junit 95 | junit 96 | ${junit.version} 97 | test 98 | 99 | 100 | 101 | org.jenkins-ci.main 102 | jenkins-core 103 | ${jenkins.version} 104 | test 105 | 106 | 107 | org.jenkins-ci.plugins.workflow 108 | workflow-step-api 109 | ${jenkins.workflow.step.version} 110 | test 111 | 112 | 113 | org.jenkins-ci.plugins.workflow 114 | workflow-cps 115 | ${jenkins.workflow.cps.version} 116 | test 117 | 118 | 119 | org.jenkins-ci 120 | symbol-annotation 121 | ${jenkins.symbol.version} 122 | test 123 | 124 | 125 | 126 | org.codehaus.groovy 127 | groovy-all 128 | ${groovy.core.version} 129 | test 130 | 131 | 132 | 133 | org.slf4j 134 | jcl-over-slf4j 135 | ${log.slf4j.version} 136 | runtime 137 | 138 | 139 | org.slf4j 140 | log4j-over-slf4j 141 | ${log.slf4j.version} 142 | runtime 143 | 144 | 145 | org.slf4j 146 | slf4j-api 147 | ${log.slf4j.version} 148 | 149 | 150 | 151 | org.spockframework 152 | spock-core 153 | ${groovy.spock.version} 154 | test 155 | 156 | 157 | 158 | 159 | org.jenkins-ci.plugins.workflow 160 | workflow-durable-task-step 161 | 2.21 162 | test 163 | 164 | 165 | 166 | 167 | org.jenkins-ci.plugins 168 | slack 169 | 2.3 170 | test 171 | 172 | 173 | 174 | 175 | org.jenkins-ci.plugins 176 | pipeline-stage-step 177 | 2.3 178 | test 179 | 180 | 181 | 182 | 183 | org.jenkins-ci.plugins 184 | ssh-agent 185 | 1.16 186 | test 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | org.apache.maven.plugins 197 | maven-surefire-plugin 198 | ${surefire.pluginVersion} 199 | 200 | 201 | default-test 202 | 203 | test 204 | 205 | 206 | 1 207 | src/test/groovy 208 | 209 | **/*Spec 210 | 211 | false 212 | 213 | ${test.loglevel} 214 | Stdout 215 | ${test.loglevel} 216 | ${logdir} 217 | 218 | 219 | 220 | 221 | 222 | 223 | org.codehaus.gmavenplus 224 | gmavenplus-plugin 225 | ${groovy.gmaven.pluginVersion} 226 | 227 | 228 | groovy 229 | 230 | addTestSources 231 | generateTestStubs 232 | compileTests 233 | removeTestStubs 234 | 235 | 236 | 237 | 238 | src/test/groovy 239 | 240 | **/*.groovy 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | org.codehaus.gmavenplus 254 | gmavenplus-plugin 255 | 256 | 257 | org.apache.maven.plugins 258 | maven-surefire-plugin 259 | 260 | 261 | 262 | 263 | 264 | 265 | Jenkinsfile 266 | *.groovy 267 | 268 | ${project.basedir} 269 | 270 | 271 | src/test/resources 272 | 273 | 274 | 275 | 276 | 277 | 278 | -------------------------------------------------------------------------------- /examples/helper-script/src/test/groovy/DeployerSpec.groovy: -------------------------------------------------------------------------------- 1 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 2 | 3 | public class DeployerSpec extends JenkinsPipelineSpecification { 4 | 5 | def Deployer = null 6 | 7 | def setup() { 8 | Deployer = loadPipelineScriptForTest("/Deployer.groovy") 9 | } 10 | 11 | def "deploy function deploys to TEST when asked" () { 12 | when: 13 | Deployer.deploy( "test" ) 14 | then: 15 | 1 * getPipelineMock("sshagent")(["test-ssh"], _ as Closure) 16 | 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-test .*/}) 17 | } 18 | 19 | def "deploy function deploys to PRODUCTION when asked" () { 20 | when: 21 | Deployer.deploy( "production" ) 22 | then: 23 | 1 * getPipelineMock("sshagent")(["prod-ssh"], _ as Closure) 24 | 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-prod .*/}) 25 | } 26 | } -------------------------------------------------------------------------------- /examples/helper-script/src/test/groovy/JenkinsfileSpec.groovy: -------------------------------------------------------------------------------- 1 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 2 | 3 | public class JenkinsfileSpec extends JenkinsPipelineSpecification { 4 | 5 | def Jenkinsfile = null 6 | 7 | public static class DummyException extends RuntimeException { 8 | public DummyException(String _message) { super( _message ); } 9 | } 10 | 11 | def setup() { 12 | Jenkinsfile = loadPipelineScriptForTest("/Jenkinsfile") 13 | Jenkinsfile.getBinding().setVariable( "scm", null ) 14 | explicitlyMockPipelineVariable("Deployer") 15 | getPipelineMock("load")("Deployer.groovy") >> { 16 | getPipelineMock("Deployer") 17 | } 18 | } 19 | 20 | def "Slack is notified when tests fail" () { 21 | setup: 22 | getPipelineMock("sh")("docker run --entrypoint python whole-pipeline -m unittest discover") >> { 23 | throw new DummyException("Dummy test failure") 24 | } 25 | when: 26 | try { 27 | Jenkinsfile.run() 28 | } catch( DummyException e ) {} 29 | then: 30 | 1 * getPipelineMock("slackSend")( _ as Map ) 31 | } 32 | 33 | def "Attempts to deploy MASTER branch to PRODUCTION" () { 34 | setup: 35 | Jenkinsfile.getBinding().setVariable( "BRANCH_NAME", "master" ) 36 | when: 37 | Jenkinsfile.run() 38 | then: 39 | 1 * getPipelineMock("Deployer.deploy")({it =~ /production/}) 40 | } 41 | 42 | def "Does NOT attempt to deploy non-MASTER branch PRODUCTION" () { 43 | setup: 44 | Jenkinsfile.getBinding().setVariable( "BRANCH_NAME", "develop" ) 45 | when: 46 | Jenkinsfile.run() 47 | then: 48 | 0 * getPipelineMock("Deployer.deploy")({it =~ /production/}) 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /examples/helper-script/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n 13 | 14 | 15 | 16 | 17 | 19 | ${logdir}/stdout.log 20 | 21 | ${logdir}/stdout.log.%i 22 | 23 | 1 24 | 10 25 | 26 | 28 | 20MB 29 | 30 | 31 | [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/shared-library/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | mvn clean verify 4 | -------------------------------------------------------------------------------- /examples/shared-library/README.md: -------------------------------------------------------------------------------- 1 | Pipeline Shared Library 2 | ============================== 3 | 4 | This project is a Jenkins Pipeline Shared Library that could be loaded by the [Pipeline Shared Groovy Libraries Plugin](https://plugins.jenkins.io/workflow-cps-global-lib). 5 | 6 | It delivers two new pipeline steps: 7 | 8 | `DefaultPipeline` 9 | ------------------------- 10 | 11 | A simple Jenkins pipeline that 12 | 13 | 1. Builds & Tests all branches 14 | 2. Notifies Slack if tests fail 15 | 3. Deploys the "master" branch to the Production environment 16 | 4. Deploys other branches to the "Test" environment. 17 | 18 | Usage: 19 | 20 | `Jenkinsfile`: 21 | 22 | ```groovy 23 | @Library("shared-library") _ 24 | DefaultPipeline() 25 | ``` 26 | 27 | `Deployer` 28 | ------------------------- 29 | 30 | A step that SSHs to a machine in an environment, and bounces an application using `docker-compose`. 31 | 32 | Usage: 33 | 34 | ``` 35 | Deployer("test") // deploy the latest version of the application to the TEST environment 36 | ``` 37 | 38 | Testing 39 | ============================== 40 | 41 | To test the library, run `make test`. 42 | 43 | Requirements 44 | ============================== 45 | 46 | The following tools should be installed in order to work with this project: 47 | 48 | 1. `make` 49 | 2. `java` 1.8+ 50 | 3. `maven` 3.3+ 51 | -------------------------------------------------------------------------------- /examples/shared-library/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | com.example 8 | jenkinsfile-test-shared-library 9 | jar 10 | 0.0.0-SNAPSHOT 11 | 12 | Testing a Pipeline Shared Library with Spock 13 | 14 | 15 | 16 | jenkins-releases 17 | Jenkins Releases 18 | http://repo.jenkins-ci.org/releases 19 | 20 | 21 | jenkins-public 22 | Jenkins Public 23 | http://repo.jenkins-ci.org/public 24 | 25 | 26 | 27 | 28 | 29 | 2.4.11 30 | 1.1-groovy-2.4 31 | 1.6.1 32 | 33 | 2.102 34 | 3.1.0 35 | 1.10 36 | 2.36 37 | 2.10 38 | 39 | 2.1.5 40 | 41 | 4.12 42 | 43 | logback-test.xml 44 | ${project.build.directory}/log 45 | ERROR 46 | 1.2.3 47 | 1.7.25 48 | 49 | 2.22.0 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | com.google.guava 58 | guava 59 | 20.0 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | com.homeaway.devtools.jenkins 68 | jenkins-spock 69 | ${jenkins-spock.version} 70 | test 71 | 72 | 73 | 74 | ch.qos.logback 75 | logback-core 76 | ${log.logback.version} 77 | runtime 78 | 79 | 80 | ch.qos.logback 81 | logback-classic 82 | ${log.logback.version} 83 | runtime 84 | 85 | 86 | 87 | javax.servlet 88 | javax.servlet-api 89 | ${jenkins.servlet.version} 90 | test 91 | 92 | 93 | 94 | junit 95 | junit 96 | ${junit.version} 97 | test 98 | 99 | 100 | 101 | org.jenkins-ci.main 102 | jenkins-core 103 | ${jenkins.version} 104 | test 105 | 106 | 107 | org.jenkins-ci.plugins.workflow 108 | workflow-step-api 109 | ${jenkins.workflow.step.version} 110 | test 111 | 112 | 113 | org.jenkins-ci.plugins.workflow 114 | workflow-cps 115 | ${jenkins.workflow.cps.version} 116 | test 117 | 118 | 119 | org.jenkins-ci 120 | symbol-annotation 121 | ${jenkins.symbol.version} 122 | test 123 | 124 | 125 | 126 | org.codehaus.groovy 127 | groovy-all 128 | ${groovy.core.version} 129 | 130 | 131 | 132 | org.slf4j 133 | jcl-over-slf4j 134 | ${log.slf4j.version} 135 | runtime 136 | 137 | 138 | org.slf4j 139 | log4j-over-slf4j 140 | ${log.slf4j.version} 141 | runtime 142 | 143 | 144 | org.slf4j 145 | slf4j-api 146 | ${log.slf4j.version} 147 | 148 | 149 | 150 | org.spockframework 151 | spock-core 152 | ${groovy.spock.version} 153 | test 154 | 155 | 156 | 157 | 158 | org.jenkins-ci.plugins.workflow 159 | workflow-durable-task-step 160 | 2.21 161 | test 162 | 163 | 164 | 165 | 166 | org.jenkins-ci.plugins 167 | slack 168 | 2.3 169 | test 170 | 171 | 172 | 173 | 174 | org.jenkins-ci.plugins 175 | pipeline-stage-step 176 | 2.3 177 | test 178 | 179 | 180 | 181 | 182 | org.jenkins-ci.plugins 183 | ssh-agent 184 | 1.16 185 | test 186 | 187 | 188 | 189 | 190 | org.jenkins-ci.plugins.workflow 191 | workflow-cps-global-lib 192 | 2.10 193 | test 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | org.apache.maven.plugins 204 | maven-surefire-plugin 205 | ${surefire.pluginVersion} 206 | 207 | 208 | default-test 209 | 210 | test 211 | 212 | 213 | 1 214 | src/test/groovy 215 | 216 | **/*Spec 217 | 218 | false 219 | 220 | ${test.loglevel} 221 | Stdout 222 | ${test.loglevel} 223 | ${logdir} 224 | 225 | 226 | 227 | 228 | 229 | 230 | org.codehaus.gmavenplus 231 | gmavenplus-plugin 232 | ${groovy.gmaven.pluginVersion} 233 | 234 | 235 | groovy 236 | 237 | addSources 238 | addTestSources 239 | generateStubs 240 | generateTestStubs 241 | compile 242 | compileTests 243 | removeStubs 244 | removeTestStubs 245 | 246 | 247 | 248 | 249 | src 250 | 251 | **/*.groovy 252 | 253 | 254 | 255 | 256 | 257 | test 258 | 259 | **/*.groovy 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | org.codehaus.gmavenplus 273 | gmavenplus-plugin 274 | 275 | 276 | org.apache.maven.plugins 277 | maven-surefire-plugin 278 | 279 | 280 | 281 | 282 | 283 | 284 | vars/**/*.groovy 285 | 286 | ${project.basedir} 287 | 288 | 289 | test/resources 290 | 291 | 292 | resources 293 | 294 | 295 | 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /examples/shared-library/resources/com/example/SlackMessageTemplate.txt: -------------------------------------------------------------------------------- 1 | Unit Tests for build ${BUILD_TAG} failed! 2 | 3 | ${BUILD_URL} 4 | -------------------------------------------------------------------------------- /examples/shared-library/src/com/example/SharedLibraryConstants.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | public class SharedLibraryConstants { 4 | public static final String DEPLOY_COMMAND = """ 5 | docker-compose pull && \ 6 | docker-compose down && \ 7 | docker-compose rm -f && \ 8 | docker-compose up -d --force-recreate""" 9 | } 10 | -------------------------------------------------------------------------------- /examples/shared-library/test/DefaultPipelineSpec.groovy: -------------------------------------------------------------------------------- 1 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 2 | 3 | public class DefaultPipelineSpec extends JenkinsPipelineSpecification { 4 | 5 | def DefaultPipeline = null 6 | 7 | public static class DummyException extends RuntimeException { 8 | public DummyException(String _message) { super( _message ); } 9 | } 10 | 11 | def setup() { 12 | DefaultPipeline = loadPipelineScriptForTest("vars/DefaultPipeline.groovy") 13 | DefaultPipeline.getBinding().setVariable( "scm", null ) 14 | getPipelineMock("libraryResource")(_) >> { 15 | return "Dummy Message" 16 | } 17 | } 18 | 19 | def "Slack is notified when tests fail" () { 20 | setup: 21 | getPipelineMock("sh")("docker run --entrypoint python whole-pipeline -m unittest discover") >> { 22 | throw new DummyException("Dummy test failure") 23 | } 24 | when: 25 | try { 26 | DefaultPipeline() 27 | } catch( DummyException e ) {} 28 | then: 29 | 1 * getPipelineMock("slackSend")( _ as Map ) 30 | } 31 | 32 | def "Attempts to deploy MASTER branch to PRODUCTION" () { 33 | setup: 34 | DefaultPipeline.getBinding().setVariable( "BRANCH_NAME", "master" ) 35 | when: 36 | DefaultPipeline() 37 | then: 38 | 1 * getPipelineMock("Deployer.call")("production") 39 | } 40 | 41 | def "Does NOT attempt to deploy non-MASTER branch PRODUCTION" () { 42 | setup: 43 | DefaultPipeline.getBinding().setVariable( "BRANCH_NAME", "develop" ) 44 | when: 45 | DefaultPipeline() 46 | then: 47 | 0 * getPipelineMock("Deployer.call")("production") 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /examples/shared-library/test/DeployerSpec.groovy: -------------------------------------------------------------------------------- 1 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 2 | 3 | public class DeployerSpec extends JenkinsPipelineSpecification { 4 | 5 | def Deployer = null 6 | 7 | def setup() { 8 | Deployer = loadPipelineScriptForTest("vars/Deployer.groovy") 9 | } 10 | 11 | def "deploy function deploys to TEST when asked" () { 12 | when: 13 | Deployer( "test" ) 14 | then: 15 | 1 * getPipelineMock("sshagent")(["test-ssh"], _ as Closure) 16 | 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-test .*/}) 17 | } 18 | 19 | def "deploy function deploys to PRODUCTION when asked" () { 20 | when: 21 | Deployer( "production" ) 22 | then: 23 | 1 * getPipelineMock("sshagent")(["prod-ssh"], _ as Closure) 24 | 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-prod .*/}) 25 | } 26 | } -------------------------------------------------------------------------------- /examples/shared-library/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n 13 | 14 | 15 | 16 | 17 | 19 | ${logdir}/stdout.log 20 | 21 | ${logdir}/stdout.log.%i 22 | 23 | 1 24 | 10 25 | 26 | 28 | 20MB 29 | 30 | 31 | [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/shared-library/vars/DefaultPipeline.groovy: -------------------------------------------------------------------------------- 1 | def call( Map _args ) { 2 | 3 | node { 4 | stage( "Checkout" ) { 5 | checkout scm 6 | } 7 | 8 | stage( "Build" ) { 9 | sh( "docker build --tag whole-pipeline ." ) 10 | } 11 | 12 | stage( "Test" ) { 13 | try { 14 | sh( "docker run --entrypoint python whole-pipeline -m unittest discover" ) 15 | } catch( Exception e ) { 16 | 17 | def message = evaluate( '"""' + libraryResource( "com/example/SlackMessageTemplate.txt" ) + '"""' ) 18 | 19 | slackSend( 20 | color: 'error', 21 | message: message ) 22 | throw e 23 | } 24 | } 25 | 26 | stage( "Push" ) { 27 | sh( "docker push whole-pipeline" ) 28 | } 29 | 30 | stage( "Deploy to TEST" ) { 31 | Deployer( "test" ) 32 | } 33 | 34 | if( BRANCH_NAME == "master" ) { 35 | stage( "Deploy to PRODUCTION" ) { 36 | Deployer( "production" ) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/shared-library/vars/Deployer.groovy: -------------------------------------------------------------------------------- 1 | import com.example.SharedLibraryConstants 2 | 3 | def call( _env ) { 4 | 5 | if( _env == "test" ) { 6 | sshagent(["test-ssh"]) { 7 | sh( "ssh deployer@app-test -c '${SharedLibraryConstants.DEPLOY_COMMAND}'" ) 8 | } 9 | } else if( _env == "production" ) { 10 | sshagent(["prod-ssh"]) { 11 | sh( "ssh deployer@app-prod -c '${SharedLibraryConstants.DEPLOY_COMMAND}'" ) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/whole-pipeline/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jfloff/alpine-python:3.6 2 | 3 | RUN pip install --upgrade \ 4 | Flask \ 5 | pip 6 | 7 | COPY app /app 8 | 9 | WORKDIR app 10 | ENV FLASK_APP=hello.py 11 | EXPOSE 5000 12 | 13 | ENTRYPOINT ["flask", "run", "--host", "0.0.0.0"] 14 | -------------------------------------------------------------------------------- /examples/whole-pipeline/Jenkinsfile: -------------------------------------------------------------------------------- 1 | def deploy( _env ) { 2 | 3 | def DEPLOY_COMMAND="docker-compose pull && docker-compose down && docker-compose rm -f && docker-compose up -d --force-recreate" 4 | 5 | if( _env == "test" ) { 6 | sshagent(["test-ssh"]) { 7 | sh( "ssh deployer@app-test -c '${DEPLOY_COMMAND}'" ) 8 | } 9 | } else if( _env == "production" ) { 10 | sshagent(["prod-ssh"]) { 11 | sh( "ssh deployer@app-prod -c '${DEPLOY_COMMAND}'" ) 12 | } 13 | } 14 | } 15 | 16 | node { 17 | stage( "Checkout" ) { 18 | checkout scm 19 | } 20 | 21 | stage( "Build" ) { 22 | sh( "docker build --tag whole-pipeline ." ) 23 | } 24 | 25 | stage( "Test" ) { 26 | try { 27 | sh( "docker run --entrypoint python whole-pipeline -m unittest discover" ) 28 | } catch( Exception e ) { 29 | slackSend( 30 | color: 'error', 31 | message: 'whole-pipeline unit tests failed.' ) 32 | throw e 33 | } 34 | } 35 | 36 | stage( "Push" ) { 37 | sh( "docker push whole-pipeline" ) 38 | } 39 | 40 | stage( "Deploy to TEST" ) { 41 | deploy( "test" ) 42 | } 43 | 44 | if( BRANCH_NAME == "master" ) { 45 | stage( "Deploy to PRODUCTION" ) { 46 | deploy( "production" ) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/whole-pipeline/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: 3 | $(MAKE) test 4 | $(MAKE) test-pipeline 5 | 6 | .PHONY: build 7 | build: 8 | docker build -t helper-script . 9 | 10 | .PHONY: run 11 | run: build 12 | docker run --rm -it --publish 5000:5000 helper-script 13 | 14 | .PHONY: test 15 | test: build 16 | docker run --entrypoint python helper-script -m unittest discover 17 | 18 | .PHONY: test-pipeline 19 | test-pipeline: 20 | mvn clean verify 21 | -------------------------------------------------------------------------------- /examples/whole-pipeline/README.md: -------------------------------------------------------------------------------- 1 | Jenkinsfile 2 | ============================== 3 | 4 | This project is a containerized Python web application with a Jenkins pipeline that 5 | 6 | 1. Builds & Tests all branches 7 | 2. Notifies Slack if tests fail 8 | 3. Deploys the "master" branch to the Production environment 9 | 4. Deploys other branches to the "Test" environment. 10 | 11 | Building 12 | ============================== 13 | 14 | To build the web application, run `make build`. 15 | 16 | Running 17 | ============================== 18 | 19 | To run the application, run `make run`. 20 | 21 | To see the running application, visit http://localhost:5000 in a web browser. 22 | 23 | Testing 24 | ============================== 25 | 26 | To test the _application_, run `make test`. 27 | 28 | To test the _pipeline_, run `make test-pipeline`. 29 | 30 | Requirements 31 | ============================== 32 | 33 | The following tools should be installed in order to work with this project: 34 | 35 | 1. `docker` 36 | 2. `make` 37 | 3. `java` 1.8+ 38 | 4. `maven` 3.3+ 39 | -------------------------------------------------------------------------------- /examples/whole-pipeline/app/counter.py: -------------------------------------------------------------------------------- 1 | def plusone(_number): 2 | return _number + 1 3 | -------------------------------------------------------------------------------- /examples/whole-pipeline/app/hello.py: -------------------------------------------------------------------------------- 1 | import counter 2 | from flask import Flask 3 | 4 | app = Flask(__name__) 5 | greeted_times = 0 6 | 7 | @app.route("/") 8 | def hello(): 9 | global greeted_times 10 | 11 | greeting = "Hello World! I've greeted {0} times!".format( greeted_times ) 12 | 13 | greeted_times = counter.plusone( greeted_times ) 14 | 15 | return greeting -------------------------------------------------------------------------------- /examples/whole-pipeline/app/test_counter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import counter 3 | 4 | class TestCounter(unittest.TestCase): 5 | def test_plusone(self): 6 | self.assertEqual(2, counter.plusone(1)) -------------------------------------------------------------------------------- /examples/whole-pipeline/app/test_hello.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import hello 3 | 4 | class TestCounter(unittest.TestCase): 5 | def test_hello(self): 6 | self.assertEqual( "Hello World! I've greeted 0 times!", hello.hello() ) 7 | self.assertEqual( "Hello World! I've greeted 1 times!", hello.hello() ) 8 | self.assertEqual( "Hello World! I've greeted 2 times!", hello.hello() ) -------------------------------------------------------------------------------- /examples/whole-pipeline/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | com.example 8 | jenkinsfile-test-whole-pipeline 9 | jar 10 | 0.0.0-SNAPSHOT 11 | 12 | Testing a Jenkinsfile with Spock 13 | 14 | 15 | 16 | jenkins-releases 17 | Jenkins Releases 18 | http://repo.jenkins-ci.org/releases 19 | 20 | 21 | jenkins-public 22 | Jenkins Public 23 | http://repo.jenkins-ci.org/public 24 | 25 | 26 | 27 | 28 | 29 | 2.4.11 30 | 1.1-groovy-2.4 31 | 1.6.1 32 | 33 | 2.102 34 | 3.1.0 35 | 1.10 36 | 2.36 37 | 2.10 38 | 39 | 2.1.5 40 | 41 | 4.12 42 | 43 | logback-test.xml 44 | ${project.build.directory}/log 45 | ERROR 46 | 1.2.3 47 | 1.7.25 48 | 49 | 2.22.0 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | com.google.guava 58 | guava 59 | 20.0 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | com.homeaway.devtools.jenkins 68 | jenkins-spock 69 | ${jenkins-spock.version} 70 | test 71 | 72 | 73 | 74 | ch.qos.logback 75 | logback-core 76 | ${log.logback.version} 77 | runtime 78 | 79 | 80 | ch.qos.logback 81 | logback-classic 82 | ${log.logback.version} 83 | runtime 84 | 85 | 86 | 87 | javax.servlet 88 | javax.servlet-api 89 | ${jenkins.servlet.version} 90 | test 91 | 92 | 93 | 94 | junit 95 | junit 96 | ${junit.version} 97 | test 98 | 99 | 100 | 101 | org.jenkins-ci.main 102 | jenkins-core 103 | ${jenkins.version} 104 | test 105 | 106 | 107 | org.jenkins-ci.plugins.workflow 108 | workflow-step-api 109 | ${jenkins.workflow.step.version} 110 | test 111 | 112 | 113 | org.jenkins-ci.plugins.workflow 114 | workflow-cps 115 | ${jenkins.workflow.cps.version} 116 | test 117 | 118 | 119 | org.jenkins-ci 120 | symbol-annotation 121 | ${jenkins.symbol.version} 122 | test 123 | 124 | 125 | 126 | org.codehaus.groovy 127 | groovy-all 128 | ${groovy.core.version} 129 | test 130 | 131 | 132 | 133 | org.slf4j 134 | jcl-over-slf4j 135 | ${log.slf4j.version} 136 | runtime 137 | 138 | 139 | org.slf4j 140 | log4j-over-slf4j 141 | ${log.slf4j.version} 142 | runtime 143 | 144 | 145 | org.slf4j 146 | slf4j-api 147 | ${log.slf4j.version} 148 | 149 | 150 | 151 | org.spockframework 152 | spock-core 153 | ${groovy.spock.version} 154 | test 155 | 156 | 157 | 158 | 159 | org.jenkins-ci.plugins.workflow 160 | workflow-durable-task-step 161 | 2.21 162 | test 163 | 164 | 165 | 166 | 167 | org.jenkins-ci.plugins 168 | slack 169 | 2.3 170 | test 171 | 172 | 173 | 174 | 175 | org.jenkins-ci.plugins 176 | pipeline-stage-step 177 | 2.3 178 | test 179 | 180 | 181 | 182 | 183 | org.jenkins-ci.plugins 184 | ssh-agent 185 | 1.16 186 | test 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | org.apache.maven.plugins 197 | maven-surefire-plugin 198 | ${surefire.pluginVersion} 199 | 200 | 201 | default-test 202 | 203 | test 204 | 205 | 206 | 1 207 | src/test/groovy 208 | 209 | **/*Spec 210 | 211 | false 212 | 213 | ${test.loglevel} 214 | Stdout 215 | ${test.loglevel} 216 | ${logdir} 217 | 218 | 219 | 220 | 221 | 222 | 223 | org.codehaus.gmavenplus 224 | gmavenplus-plugin 225 | ${groovy.gmaven.pluginVersion} 226 | 227 | 228 | groovy 229 | 230 | addTestSources 231 | generateTestStubs 232 | compileTests 233 | removeTestStubs 234 | 235 | 236 | 237 | 238 | src/test/groovy 239 | 240 | **/*.groovy 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | org.codehaus.gmavenplus 254 | gmavenplus-plugin 255 | 256 | 257 | org.apache.maven.plugins 258 | maven-surefire-plugin 259 | 260 | 261 | 262 | 263 | 264 | 265 | Jenkinsfile 266 | 267 | ${project.basedir} 268 | 269 | 270 | src/test/resources 271 | 272 | 273 | 274 | 275 | 276 | 277 | -------------------------------------------------------------------------------- /examples/whole-pipeline/src/test/groovy/JenkinsfileSpec.groovy: -------------------------------------------------------------------------------- 1 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 2 | 3 | public class JenkinsfileSpec extends JenkinsPipelineSpecification { 4 | 5 | def Jenkinsfile = null 6 | 7 | public static class DummyException extends RuntimeException { 8 | public DummyException(String _message) { super( _message ); } 9 | } 10 | 11 | def setup() { 12 | Jenkinsfile = loadPipelineScriptForTest("/Jenkinsfile") 13 | Jenkinsfile.getBinding().setVariable( "scm", null ) 14 | } 15 | 16 | def "Slack is notified when tests fail" () { 17 | setup: 18 | getPipelineMock("sh")("docker run --entrypoint python whole-pipeline -m unittest discover") >> { 19 | throw new DummyException("Dummy test failure") 20 | } 21 | when: 22 | try { 23 | Jenkinsfile.run() 24 | } catch( DummyException e ) {} 25 | then: 26 | 1 * getPipelineMock("slackSend")( _ as Map ) 27 | } 28 | 29 | def "Attempts to deploy MASTER branch to PRODUCTION" () { 30 | setup: 31 | Jenkinsfile.getBinding().setVariable( "BRANCH_NAME", "master" ) 32 | when: 33 | Jenkinsfile.run() 34 | then: 35 | 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-prod .*/}) 36 | } 37 | 38 | def "Does NOT attempt to deploy non-MASTER branch PRODUCTION" () { 39 | setup: 40 | Jenkinsfile.getBinding().setVariable( "BRANCH_NAME", "develop" ) 41 | when: 42 | Jenkinsfile.run() 43 | then: 44 | 0 * getPipelineMock("sh")({it =~ /ssh deployer@app-prod .*/}) 45 | } 46 | 47 | def "deploy function deploys to TEST when asked" () { 48 | when: 49 | Jenkinsfile.deploy( "test" ) 50 | then: 51 | 1 * getPipelineMock("sshagent")(["test-ssh"], _ as Closure) 52 | 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-test .*/}) 53 | } 54 | 55 | def "deploy function deploys to PRODUCTION when asked" () { 56 | when: 57 | Jenkinsfile.deploy( "production" ) 58 | then: 59 | 1 * getPipelineMock("sshagent")(["prod-ssh"], _ as Closure) 60 | 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-prod .*/}) 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /examples/whole-pipeline/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n 13 | 14 | 15 | 16 | 17 | 19 | ${logdir}/stdout.log 20 | 21 | ${logdir}/stdout.log.%i 22 | 23 | 1 24 | 10 25 | 26 | 28 | 20MB 29 | 30 | 31 | [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.homeaway.devtools.jenkins 7 | jenkins-spock 8 | jar 9 | 2.1.6-SNAPSHOT 10 | 11 | Jenkins pipeline support for Spock 12 | Test Jenkins pipeline code with Spock 13 | https://github.com/homeaway/jenkins-spock 14 | 15 | 16 | 17 | The Apache Software License, Version 2.0 18 | http://www.apache.org/licenses/LICENSE-2.0.txt 19 | repo 20 | 21 | 22 | 23 | 24 | 25 | awitt 26 | Austin Witt 27 | awitt@vrbo.com 28 | HomeAway 29 | -6 30 | 31 | 32 | 33 | 34 | https://github.com/homeaway/jenkins-spock 35 | scm:git:git@github.com:homeaway/jenkins-spock.git 36 | scm:git:git@github.com:homeaway/jenkins-spock.git 37 | HEAD 38 | 39 | 40 | 41 | 42 | jenkins-releases 43 | Jenkins Releases 44 | http://repo.jenkins-ci.org/releases 45 | 46 | 47 | 48 | 49 | 50 | ossrh 51 | https://oss.sonatype.org/content/repositories/snapshots 52 | 53 | 54 | ossrh 55 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 56 | 57 | 58 | 59 | 60 | 61 | 1.8 62 | 1.8 63 | UTF-8 64 | UTF-8 65 | 66 | 3.1.0 67 | 3.1.0 68 | 3.8.0 69 | 2.8.2 70 | 3.0.0-M2 71 | 1.6.0 72 | 1.5 73 | 2.5.2 74 | 3.2.1 75 | 3.1.0 76 | 3.0.1 77 | 1.6.7 78 | 2.5.3 79 | 3.1.0 80 | 1.11.2 81 | 3.7.1 82 | 2.2.1 83 | 2.22.0 84 | 85 | 86 | 2.4.11 87 | 1.1-groovy-2.4 88 | 1.6.1 89 | 90 | 91 | 3.2.7 92 | 2.6 93 | 94 | 95 | 2.102 96 | 2.10 97 | 2.36 98 | 1.10 99 | 3.1.0 100 | 101 | 102 | 1.7.25 103 | 4.12 104 | 105 | logback-test.xml 106 | ${project.build.directory}/log 107 | WARN 108 | 1.2.3 109 | 1.7.25 110 | 111 | 112 | 113 | 114 | 115 | it 116 | 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-invoker-plugin 122 | ${invoker.pluginVersion} 123 | 124 | 125 | fixup-integration-test-projects 126 | process-resources 127 | 128 | run 129 | 130 | 131 | examples 132 | 133 | helper-script/pom.xml 134 | shared-library/pom.xml 135 | whole-pipeline/pom.xml 136 | 137 | ${project.build.directory}/test-projects 138 | true 139 | versions:update-properties 140 | 141 | true 142 | ${project.groupId}:${project.artifactId} 143 | jenkins-spock.version 144 | ${project.version} 145 | false 146 | 147 | 148 | 149 | 150 | run-integration-test-projects 151 | 152 | install 153 | integration-test 154 | verify 155 | 156 | 157 | ${project.build.directory}/test-projects 158 | ${project.build.directory}/test-projects-repo 159 | 160 | true 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | org.apache.maven.plugins 172 | maven-resources-plugin 173 | 174 | 175 | 176 | org.apache.maven.plugins 177 | maven-invoker-plugin 178 | 179 | 180 | 181 | 182 | 183 | 184 | noninteractive-gpg 185 | 186 | 187 | 188 | 189 | org.apache.maven.plugins 190 | maven-gpg-plugin 191 | ${gpg.pluginVersion} 192 | 193 | 194 | sign-artifacts 195 | 196 | 197 | --pinentry-mode 198 | loopback 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | gh-pages-javadoc 211 | 212 | 213 | performRelease 214 | 215 | 216 | 217 | 218 | 219 | 220 | org.apache.maven.plugins 221 | maven-scm-publish-plugin 222 | 1.1 223 | 224 | 225 | publish-docs 226 | deploy 227 | 228 | publish-scm 229 | 230 | 231 | ${metadata.isPom} 232 | ${project.build.directory}/scmpublish 233 | Publishing javadoc for ${project.artifactId}:${project.version} 234 | ${project.reporting.outputDirectory}/apidocs 235 | true 236 | ${project.scm.connection} 237 | gh-pages 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | org.apache.maven.plugins 247 | maven-scm-publish-plugin 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | org.spockframework 259 | spock-core 260 | ${groovy.spock.version} 261 | 262 | 263 | 264 | cglib 265 | cglib-nodep 266 | ${cglib.version} 267 | 268 | 269 | org.objenesis 270 | objenesis 271 | ${objenesis.version} 272 | 273 | 274 | 275 | org.reflections 276 | reflections 277 | 0.9.12 278 | 279 | 280 | 281 | io.github.classgraph 282 | classgraph 283 | 4.8.104 284 | 285 | 286 | 287 | org.codehaus.groovy 288 | groovy-all 289 | ${groovy.core.version} 290 | 291 | 292 | 293 | 294 | 295 | 296 | org.slf4j 297 | log4j-over-slf4j 298 | ${log.slf4j.version} 299 | runtime 300 | 301 | 302 | org.slf4j 303 | jcl-over-slf4j 304 | ${log.slf4j.version} 305 | runtime 306 | 307 | 308 | 309 | 310 | 311 | ch.qos.logback 312 | logback-core 313 | ${log.logback.version} 314 | runtime 315 | 316 | 317 | ch.qos.logback 318 | logback-classic 319 | ${log.logback.version} 320 | runtime 321 | 322 | 323 | 324 | 325 | org.slf4j 326 | slf4j-api 327 | ${log.slf4j.version} 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | org.jenkins-ci.main 337 | jenkins-core 338 | ${jenkins.version} 339 | provided 340 | 341 | 342 | log4j 343 | log4j 344 | 345 | 346 | commons-logging 347 | commons-logging 348 | 349 | 350 | 351 | 352 | org.jenkins-ci.plugins.workflow 353 | workflow-step-api 354 | ${jenkins.workflow.step.version} 355 | provided 356 | 357 | 358 | org.jenkins-ci.plugins.workflow 359 | workflow-cps 360 | ${jenkins.workflow.cps.version} 361 | provided 362 | 363 | 364 | org.jenkins-ci 365 | symbol-annotation 366 | ${jenkins.symbol.version} 367 | provided 368 | 369 | 370 | 371 | javax.servlet 372 | javax.servlet-api 373 | ${jenkins.servlet.version} 374 | provided 375 | 376 | 377 | 378 | 379 | 380 | junit 381 | junit 382 | ${junit.version} 383 | provided 384 | 385 | 386 | 387 | 388 | 389 | 390 | org.jenkins-ci.plugins.workflow 391 | workflow-durable-task-step 392 | 2.17 393 | test 394 | 395 | 396 | 397 | 398 | org.jenkins-ci.plugins.workflow 399 | workflow-basic-steps 400 | 2.6 401 | test 402 | 403 | 404 | 405 | 406 | org.jenkins-ci.plugins 407 | docker-workflow 408 | 1.14 409 | test 410 | 411 | 412 | 413 | 414 | org.jenkins-ci.plugins 415 | pipeline-stage-step 416 | 2.3 417 | test 418 | 419 | 420 | 421 | 422 | 427 | 428 | org.mockito 429 | mockito-all 430 | 1.10.19 431 | test 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | org.apache.maven.plugins 442 | maven-assembly-plugin 443 | ${assembly.pluginVersion} 444 | 445 | 446 | javadoc 447 | package 448 | 449 | single 450 | 451 | 452 | 453 | src/main/assembly/javadoc.xml 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | org.apache.maven.plugins 462 | maven-clean-plugin 463 | ${clean.pluginVersion} 464 | 465 | 466 | 467 | org.apache.maven.plugins 468 | maven-compiler-plugin 469 | ${compiler.pluginVersion} 470 | 471 | ${sourceVersion} 472 | ${targetVersion} 473 | 474 | 475 | 476 | 477 | maven-dependency-plugin 478 | 479 | 480 | 482 | cglib:cglib-nodep 483 | junit:junit 484 | org.objenesis:objenesis 485 | 486 | 487 | 488 | 489 | 490 | org.apache.maven.plugins 491 | maven-deploy-plugin 492 | ${deploy.pluginVersion} 493 | 494 | 495 | 496 | maven-enforcer-plugin 497 | ${enforcer.pluginVersion} 498 | 499 | 500 | enforce-plugin-versions 501 | 502 | enforce 503 | 504 | 505 | 506 | 507 | You MUST always define plugin versions! 508 | false 509 | true 510 | false 511 | clean,test,install,deploy,site-deploy 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | org.apache.maven.plugins 521 | maven-gpg-plugin 522 | ${gpg.pluginVersion} 523 | 524 | 525 | sign-artifacts 526 | verify 527 | 528 | sign 529 | 530 | 531 | 532 | 533 | 534 | 535 | org.apache.maven.plugins 536 | maven-install-plugin 537 | ${install.pluginVersion} 538 | 539 | 540 | 541 | org.apache.maven.plugins 542 | maven-jar-plugin 543 | ${jar.pluginVersion} 544 | 545 | 546 | 547 | org.apache.maven.plugins 548 | maven-javadoc-plugin 549 | ${javadoc.pluginVersion} 550 | 551 | true 552 | 553 | 554 | 555 | 556 | org.apache.maven.plugins 557 | maven-release-plugin 558 | ${release.pluginVersion} 559 | 560 | clean exec:exec@update-maven-example-versions scm:add@checkin-updated-examples scm:checkin@checkin-updated-examples verify 561 | 562 | 563 | 564 | 565 | org.apache.maven.plugins 566 | maven-resources-plugin 567 | ${resources.pluginVersion} 568 | 569 | 570 | 571 | org.apache.maven.plugins 572 | maven-scm-plugin 573 | ${scm.pluginVersion} 574 | 575 | 576 | checkin-updated-examples 577 | 578 | add 579 | checkin 580 | 581 | 582 | **/*.xml 583 | **/target/ 584 | [automated] Updating examples to jenkins-spock:${project.version} 585 | 586 | 587 | 588 | 589 | 590 | 591 | org.apache.maven.plugins 592 | maven-site-plugin 593 | ${site.pluginVersion} 594 | 595 | 596 | org.apache.maven.doxia 597 | doxia-module-markdown 598 | 1.3 599 | 600 | 601 | 602 | 603 | 604 | org.apache.maven.plugins 605 | maven-source-plugin 606 | ${source.pluginVersion} 607 | 608 | 609 | attach-sources 610 | 611 | jar-no-fork 612 | test-jar-no-fork 613 | 614 | 615 | 616 | 617 | 618 | 619 | org.apache.maven.plugins 620 | maven-surefire-plugin 621 | ${surefire.pluginVersion} 622 | 623 | 624 | default-test 625 | 626 | test 627 | 628 | 629 | src/test/groovy 630 | 631 | **/*Spec 632 | 633 | false 634 | 635 | ${test.loglevel} 636 | Stdout 637 | ${test.loglevel} 638 | ${logdir} 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | org.codehaus.gmavenplus 647 | gmavenplus-plugin 648 | ${groovy.gmaven.pluginVersion} 649 | 650 | 651 | groovy 652 | 653 | addSources 654 | addTestSources 655 | generateStubs 656 | compile 657 | generateTestStubs 658 | compileTests 659 | removeStubs 660 | removeTestStubs 661 | 662 | 663 | 664 | 665 | src/main/groovy 666 | 667 | **/*.groovy 668 | 669 | 670 | 671 | 672 | 673 | src/test/groovy 674 | 675 | **/*.groovy 676 | 677 | 678 | 679 | 680 | 681 | 682 | groovydoc 683 | 684 | groovydoc 685 | 686 | prepare-package 687 | 688 | 689 | 690 | https://docs.oracle.com/javase/8/docs/api/ 691 | java,javax 692 | 693 | 694 | http://docs.groovy-lang.org/${groovy.core.version}/html/gapi/ 695 | groovy 696 | 697 | 698 | http://spockframework.org/spock/javadoc/1.1-rc-4/ 699 | spock 700 | 701 | 702 | http://javadoc.jenkins.io/ 703 | hudson,jenkins 704 | 705 | 706 | http://javadoc.jenkins.io/plugin/workflow-cps/ 707 | org.jenkinsci.plugins.workflow.cps 708 | 709 | 710 | http://javadoc.jenkins.io/plugin/workflow-step-api/ 711 | org.jenkinsci.plugins.workflow.steps,org.jenkinsci.plugins.workflow.structs,org.jenkinsci.plugins.workflow.util 712 | 713 | 714 | http://javadoc.jenkins.io/plugin/workflow-api/ 715 | org.jenkinsci.plugins.workflow 716 | 717 | 718 | ${project.reporting.outputDirectory}/apidocs 719 | protected 720 | 721 | 722 | 723 | 724 | 725 | 726 | org.codehaus.mojo 727 | exec-maven-plugin 728 | ${exec.pluginVersion} 729 | 730 | 731 | update-maven-example-versions 732 | 733 | exec 734 | 735 | 736 | ${project.basedir}/examples 737 | sh 738 | 739 | -c 740 | " --include="*.xml" . | xargs gawk -i inplace '{ gsub(/[^<]+<\/jenkins-spock.version>/, "${project.version}") }; { print }' 742 | ]]> 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | org.sonatype.plugins 752 | nexus-staging-maven-plugin 753 | ${nexusStaging.pluginVersion} 754 | true 755 | 756 | ossrh 757 | https://oss.sonatype.org/ 758 | true 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | org.apache.maven.plugins 770 | maven-clean-plugin 771 | 772 | 773 | 774 | 775 | 776 | org.codehaus.gmavenplus 777 | gmavenplus-plugin 778 | 779 | 780 | 781 | org.apache.maven.plugins 782 | maven-source-plugin 783 | 784 | 785 | 786 | org.apache.maven.plugins 787 | maven-compiler-plugin 788 | 789 | 790 | 791 | org.apache.maven.plugins 792 | maven-surefire-plugin 793 | 794 | 795 | 796 | org.apache.maven.plugins 797 | maven-assembly-plugin 798 | 799 | 800 | 801 | org.apache.maven.plugins 802 | maven-enforcer-plugin 803 | 804 | 805 | 806 | org.apache.maven.plugins 807 | maven-gpg-plugin 808 | 809 | 810 | 811 | org.apache.maven.plugins 812 | maven-install-plugin 813 | 814 | 815 | 816 | org.apache.maven.plugins 817 | maven-deploy-plugin 818 | 819 | 820 | 821 | org.sonatype.plugins 822 | nexus-staging-maven-plugin 823 | 824 | 825 | 826 | org.apache.maven.plugins 827 | maven-site-plugin 828 | 829 | 830 | 831 | 832 | -------------------------------------------------------------------------------- /src/main/assembly/javadoc.xml: -------------------------------------------------------------------------------- 1 | 5 | javadoc 6 | false 7 | 8 | 9 | jar 10 | 11 | 12 | 13 | ${project.build.directory}/site/apidocs/ 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/groovy/com/homeaway/devtools/jenkins/testing/InvalidlyNamedScriptWrapper.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package com.homeaway.devtools.jenkins.testing 18 | 19 | import org.codehaus.groovy.runtime.InvokerHelper 20 | 21 | /** 22 | * Wraps a {@link Script} whose filename would not be a valid JVM class name. 23 | *

24 | * Redirects all {@link Script} interface methods to the wrapped Script, calling them 25 | * indirectly to prevent their names from causing problems with Java classloaders. 26 | *

27 | * 28 | * @see GROOVY-7670 29 | * 30 | * @author awitt 31 | * @since 2.1.1 32 | */ 33 | public class InvalidlyNamedScriptWrapper extends Script { 34 | 35 | protected Script script 36 | 37 | public InvalidlyNamedScriptWrapper(Script _script) { 38 | script = _script 39 | } 40 | 41 | @Override 42 | public Binding getBinding() { 43 | return script.getBinding() 44 | } 45 | 46 | @Override 47 | public void setBinding(Binding _binding) { 48 | script.setBinding( _binding ) 49 | } 50 | 51 | @Override 52 | public Object getProperty(String _property) { 53 | return script.getProperty( _property ) 54 | } 55 | 56 | @Override 57 | public void setProperty(String _property, Object _newValue) { 58 | script.setProperty( _property, _newValue ) 59 | } 60 | 61 | @Override 62 | public Object run() { 63 | return InvokerHelper.invokeMethod( script, "run", null ) 64 | } 65 | 66 | @Override 67 | public Object invokeMethod(String _name, Object _args) { 68 | return InvokerHelper.invokeMethod( script, _name, _args ) 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/groovy/com/homeaway/devtools/jenkins/testing/PipelineVariableImpersonator.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing 19 | 20 | import org.slf4j.Logger 21 | import org.slf4j.LoggerFactory 22 | 23 | /** 24 | * Takes the place of a {@link GlobalVariable} in a {@link JenkinsPipelineSpecification} context. 25 | * Intercepts every method call and forwards it to a mock for the current test-run. 26 | * Mocks are named with the pattern GlobalVariableName.methodName - for example, in the following pipeline script under test: 27 | *

 28 | SomeVariable.someFunction("foo")
 29 |  * 
30 | * The someFunction call could be verified by 31 | *

 32 | then:
 33 | 	1 * getPipelineMock("SomeVariable.someFunction")("foo")
 34 |  * 
35 | * 36 | * @author awitt 37 | * 38 | */ 39 | public class PipelineVariableImpersonator { 40 | 41 | private static final Logger LOG = LoggerFactory.getLogger(PipelineVariableImpersonator.class) 42 | 43 | /** 44 | * The "Global Variable" property name to automatically generate mocks for. 45 | */ 46 | protected String for_property 47 | 48 | /** 49 | * The test suite that in which to automatically generate mocks. 50 | */ 51 | protected JenkinsPipelineSpecification for_spec 52 | 53 | /** 54 | * Create a stand-in for _property in the Spock _spec 55 | * @param _property The "Global Variable" property name to automatically generate mocks for. 56 | * @param _spec The test suite that in which to automatically generate mocks. 57 | */ 58 | public PipelineVariableImpersonator(String _property, JenkinsPipelineSpecification _spec) { 59 | for_property = _property 60 | for_spec = _spec 61 | } 62 | 63 | /** 64 | * Intercept a missing method invocation on this "Global Variable" and try calling a mock with that name, instead. 65 | * 66 | * @param _name The name of the method that didn't exist 67 | * @param _args The arguments to that method 68 | * 69 | * @return The results of invoking {@link #for_spec}.getPipelineMock( "{@link #for_property}._name" )( _args ) 70 | */ 71 | def methodMissing(String _name, _args) { 72 | 73 | def prepared_args = _args 74 | 75 | if( ! ( prepared_args instanceof Object[] ) ) { 76 | // this method took one argument. Later on, unwrapping it will be WRONG, so, just pre-wrap it. 77 | prepared_args = [_args].toArray() 78 | } 79 | 80 | String key = "${for_property}.${_name}" 81 | 82 | Closure mock = for_spec.explicitlyMockPipelineStep( key, "(implicit-runtime) getPipelineMock(\"${key}\")" ) 83 | 84 | JenkinsPipelineSpecification.LOG_CALL_INTERCEPT( 85 | "debug", 86 | "method call on pipeline variable [${for_property}]", 87 | this, 88 | "methodMissing", 89 | _name, 90 | _args, 91 | mock, 92 | "call", 93 | prepared_args, 94 | true ) 95 | 96 | // use the actual closure Mock 97 | Object result = mock( *prepared_args ) 98 | 99 | if( _args != null && _args.length >= 1 && _args[_args.length-1] instanceof Closure ) { 100 | // there was at least one argument, and the last argument was a Closure 101 | // this almost certainly means that the user's trying to pass the closure to the function as a "body" 102 | // they probably want it to execute right now. 103 | _args[_args.length-1]() 104 | } 105 | 106 | return result 107 | } 108 | 109 | def propertyMissing(String _name) { 110 | return for_spec.getPipelineMock( "${for_property}.getProperty" )( _name ) 111 | } 112 | 113 | public String toString() { 114 | return "Mock Generator for [${for_property}]" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/homeaway/devtools/jenkins/testing/APipelineExtensionDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing; 19 | 20 | import java.lang.annotation.Annotation; 21 | import java.util.Arrays; 22 | import java.util.HashMap; 23 | import java.util.HashSet; 24 | import java.util.Map; 25 | import java.util.Map.Entry; 26 | import java.util.Optional; 27 | import java.util.Set; 28 | 29 | import org.jenkinsci.Symbol; 30 | import org.jenkinsci.plugins.workflow.cps.GlobalVariable; 31 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | 35 | import hudson.Extension; 36 | 37 | /** 38 | * Utility class for detecting extension points to Jenkins' Pipeline API. 39 | * 40 | * @author awitt 41 | * 42 | */ 43 | public abstract class APipelineExtensionDetector { 44 | 45 | private static final Logger LOG = LoggerFactory.getLogger( APipelineExtensionDetector.class ); 46 | 47 | /** 48 | * Find classes of the provided _supertype in the provided _package tree. 49 | * 50 | * @param _supertype The type of class to look for. 51 | * @param _package The package tree to look in 52 | * 53 | * @return classes of the provided _supertype in the provided _package tree. 54 | */ 55 | public abstract Set> getClassesOfTypeInPackage(Class _supertype, Optional _package); 56 | 57 | /** 58 | * Find classes annotated with the given _annotation that are subtypes of the provided _supertype, in the provided _package tree. 59 | * 60 | * @param _annotation The type of annotation to look for. 61 | * @param _supertype The type of class to look for. 62 | * @param _package The package tree to look in 63 | * 64 | * @return classes annotated with the given _annotation that are subtypes of the provided _supertype, in the provided _package tree. 65 | */ 66 | public abstract Set> getClassesWithAnnotationOfTypeInPackage(Class _annotation, Class _supertype, Optional _package); 67 | 68 | /** 69 | * Find all of the {@link StepDescriptor} extensions in the _package. 70 | * 71 | * @param _package The package to look in for {@link StepDescriptor} extensions. 72 | * 73 | * @return all of the {@link StepDescriptor} extensions in the _package. 74 | */ 75 | public Set getPipelineSteps(Optional _package) { 76 | 77 | Set names = new HashSet<>(); 78 | 79 | Map failures = new HashMap<>(); 80 | 81 | for( Class step_descriptor_class : getClassesWithAnnotationOfTypeInPackage(Extension.class, StepDescriptor.class, _package ) ) { 82 | try { 83 | StepDescriptor descriptor = (StepDescriptor) step_descriptor_class.newInstance(); 84 | 85 | names.add( descriptor.getFunctionName() ); 86 | } catch( InstantiationException e ) { 87 | failures.put( step_descriptor_class.getName(), e ); 88 | continue; 89 | } catch( IllegalAccessException e ) { 90 | failures.put( step_descriptor_class.getName(), e ); 91 | continue; 92 | } 93 | } 94 | 95 | if( failures.size() > 0 ) { 96 | if( "true".equals( System.getProperty( "PipelineExtensionDetector.expandFailures" ) ) ) { 97 | for( Entry failure : failures.entrySet() ) { 98 | LOG.error( failure.getKey(), failure.getValue() ); 99 | } 100 | } else { 101 | LOG.error( 102 | "Failed to get the function names of the following StepDescriptor classes. For detailed error messages, set the system property PipelineExtensionDetector.expandFailures=true. Failures: [{}]", 103 | failures.keySet() ); 104 | } 105 | } 106 | 107 | return names; 108 | } 109 | 110 | /** 111 | * Find all of the {@link GlobalVariable} extensions in the _package. 112 | * 113 | * @param _package The package to look in for {@link GlobalVariable} extensions. 114 | * 115 | * @return all of the {@link GlobalVariable} extensions in the _package. 116 | */ 117 | public Set getGlobalVariables(Optional _package) { 118 | 119 | Set names = new HashSet<>(); 120 | 121 | Map failures = new HashMap<>(); 122 | 123 | for( Class global_variable_class : getClassesWithAnnotationOfTypeInPackage(Extension.class, GlobalVariable.class, _package ) ) { 124 | try { 125 | GlobalVariable variable = (GlobalVariable) global_variable_class.newInstance(); 126 | 127 | names.add( variable.getName() ); 128 | } catch( InstantiationException e ) { 129 | failures.put( global_variable_class.getName(), e ); 130 | continue; 131 | } catch( IllegalAccessException e ) { 132 | failures.put( global_variable_class.getName(), e ); 133 | continue; 134 | } 135 | } 136 | 137 | if( failures.size() > 0 ) { 138 | if( "true".equals( System.getProperty( "PipelineExtensionDetector.expandFailures" ) ) ) { 139 | for( Entry failure : failures.entrySet() ) { 140 | LOG.error( failure.getKey(), failure.getValue() ); 141 | } 142 | } else { 143 | LOG.error( 144 | "Failed to get the names of the following GlobalVariable classes. For detailed error messages, set the system property PipelineExtensionDetector.expandFailures=true. Failures: [{}]", 145 | failures.keySet() ); 146 | } 147 | } 148 | 149 | return names; 150 | } 151 | 152 | /** 153 | * Find all of the {@link Symbol} extensions in the _package. 154 | * 155 | * @param _package The package to look in for {@link Symbol} extensions. 156 | * 157 | * @return all of the {@link Symbol} extensions in the _package. 158 | */ 159 | public Set getSymbols(Optional _package) { 160 | 161 | Set names = new HashSet<>(); 162 | 163 | for( Class symbol_class : getClassesWithAnnotationOfTypeInPackage( Symbol.class, Object.class, _package ) ) { 164 | 165 | Symbol symbol = symbol_class.getAnnotation( Symbol.class ); 166 | names.addAll( Arrays.asList( symbol.value() ) ); 167 | 168 | } 169 | 170 | return names; 171 | } 172 | 173 | public Set getPipelineExtensions(Optional _package) { 174 | 175 | Set names = new HashSet<>(); 176 | 177 | names.addAll( getPipelineSteps( _package ) ); 178 | names.addAll( getGlobalVariables( _package ) ); 179 | names.addAll( getSymbols( _package ) ); 180 | 181 | return names; 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/com/homeaway/devtools/jenkins/testing/LocalProjectPipelineExtensionDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing; 19 | 20 | import java.lang.annotation.Annotation; 21 | import java.util.HashMap; 22 | import java.util.HashSet; 23 | import java.util.Map; 24 | import java.util.Map.Entry; 25 | import java.util.Optional; 26 | import java.util.Set; 27 | 28 | import org.reflections.Reflections; 29 | import org.reflections.ReflectionsException; 30 | import org.reflections.scanners.SubTypesScanner; 31 | import org.reflections.util.ClasspathHelper; 32 | import org.reflections.util.ConfigurationBuilder; 33 | import org.slf4j.Logger; 34 | import org.slf4j.LoggerFactory; 35 | 36 | /** 37 | * Search through classes in the local project for Jenkins pipeline extensions. Assumes 38 | *
    39 | *
  1. The local project is built with maven
  2. 40 | *
  3. This class is only used during test runs initiated by the maven-surefire-plugin
  4. 41 | *
42 | * Uses the Reflections metadata analysis engine. 43 | * 44 | * @author awitt 45 | * 46 | */ 47 | public class LocalProjectPipelineExtensionDetector extends APipelineExtensionDetector { 48 | 49 | private static final Logger LOG = LoggerFactory.getLogger( LocalProjectPipelineExtensionDetector.class ); 50 | 51 | @Override 52 | public Set> getClassesOfTypeInPackage(Class _supertype, Optional _package) { 53 | 54 | Set> classes = new HashSet<>(); 55 | 56 | Map failures = new HashMap<>(); 57 | 58 | Reflections reflector = new Reflections( 59 | new ConfigurationBuilder() 60 | .setScanners(new SubTypesScanner(false)) 61 | .setUrls(ClasspathHelper.forPackage(_package.orElse("")))); 62 | 63 | Set all_types = new HashSet<>(); 64 | 65 | try { 66 | all_types = reflector.getAllTypes(); 67 | } catch( ReflectionsException re ) { 68 | if( re.getMessage().contains( "Couldn't find subtypes of Object." ) ) { 69 | // this can happen if there are no classes local to the project. 70 | // the full error message is 71 | // Couldn't find subtypes of Object. 72 | // Make sure SubTypesScanner initialized to include Object class - new SubTypesScanner(false) 73 | // which we have done above, so that is NOT the actual cause. 74 | // If there are no classes local to the project, that's OK! 75 | // Just use an empty set instead of throwing an exception. 76 | LOG.info( "Looks like there aren't any classes compiled by this project." ); 77 | } else { 78 | // We still do want to error with "real" exceptions, though. 79 | throw re; 80 | } 81 | } 82 | 83 | for(String classname : all_types ) { 84 | 85 | Class clazz = null; 86 | 87 | try { 88 | clazz = Class.forName( classname ); 89 | } catch( ClassNotFoundException | NoClassDefFoundError e ) { 90 | failures.put( classname, e ); 91 | continue; 92 | } 93 | 94 | classes.add( clazz ); 95 | } 96 | 97 | if( failures.size() > 0 ) { 98 | if( "true".equals( System.getProperty( "PipelineExtensionDetector.expandFailures" ) ) ) { 99 | for( Entry failure : failures.entrySet() ) { 100 | LOG.error( failure.getKey(), failure.getValue() ); 101 | } 102 | } else { 103 | LOG.error( 104 | "Failed to get some classes of type [{}] in package [{}]. For detailed error messages, set the system property PipelineExtensionDetector.expandFailures=true. Failures: [{}]", 105 | _supertype, 106 | _package, 107 | failures.keySet() ); 108 | } 109 | } 110 | 111 | return classes; 112 | } 113 | 114 | @Override 115 | public Set> getClassesWithAnnotationOfTypeInPackage( Class _annotation, Class _supertype, Optional _package) { 116 | 117 | Set> annotated_classes = new HashSet<>(); 118 | 119 | for( Class annotated_class : new Reflections( _package.orElse("") ).getTypesAnnotatedWith( _annotation ) ) { 120 | if( _supertype.isAssignableFrom( annotated_class ) ) { 121 | annotated_classes.add( annotated_class ); 122 | } 123 | } 124 | 125 | return annotated_classes; 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/homeaway/devtools/jenkins/testing/WholeClasspathPipelineExtensionDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing; 19 | 20 | import java.lang.annotation.Annotation; 21 | import java.util.HashMap; 22 | import java.util.HashSet; 23 | import java.util.List; 24 | import java.util.Map.Entry; 25 | import java.util.Optional; 26 | import java.util.Set; 27 | 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | import io.github.classgraph.ClassGraph; 32 | 33 | /** 34 | * Search through classes in the entire classpath for Jenkins Pipeline extensions. 35 | * Uses the FastClasspathScanner metadata analysis engine. 36 | * 37 | * @author awitt 38 | * 39 | */ 40 | public class WholeClasspathPipelineExtensionDetector extends APipelineExtensionDetector { 41 | 42 | private static final Logger LOG = LoggerFactory.getLogger( WholeClasspathPipelineExtensionDetector.class ); 43 | 44 | @Override 45 | public Set> getClassesOfTypeInPackage(Class _supertype, Optional _package) { 46 | 47 | Set> classes = new HashSet<>(); 48 | 49 | List classnames = new ClassGraph() 50 | .enableAnnotationInfo() 51 | .enableClassInfo() 52 | .acceptPackages(_package.orElse("")) 53 | .scan().getAllStandardClasses().getNames(); 54 | 55 | HashMap failures = new HashMap<>(); 56 | 57 | for(String classname: classnames) { 58 | 59 | Class clazz = null; 60 | 61 | try { 62 | clazz = Class.forName( classname ); 63 | } catch( ClassNotFoundException e ) { 64 | failures.put( classname, e ); 65 | continue; 66 | } catch( Throwable t ) { 67 | // probably BS static initialization; hope you don't need to mock this class... 68 | failures.put( classname, t ); 69 | continue; 70 | } 71 | 72 | if( _supertype.isAssignableFrom( clazz ) ) { 73 | classes.add( clazz ); 74 | } 75 | } 76 | 77 | if( failures.size() > 0 ) { 78 | if( "true".equals( System.getProperty( "PipelineExtensionDetector.expandFailures" ) ) ) { 79 | for( Entry failure : failures.entrySet() ) { 80 | LOG.error( failure.getKey(), failure.getValue() ); 81 | } 82 | } else { 83 | LOG.warn( 84 | "Failed to get some classes of type [{}] in package [{}]. For detailed error messages, set the system property PipelineExtensionDetector.expandFailures=true. Failures: [{}]", 85 | _supertype, 86 | _package, 87 | failures.keySet() ); 88 | } 89 | } 90 | 91 | return classes; 92 | } 93 | 94 | @Override 95 | public Set> getClassesWithAnnotationOfTypeInPackage( Class _annotation, Class _supertype, Optional _package) { 96 | 97 | Set> annotated_classes = new HashSet<>(); 98 | 99 | HashMap failures = new HashMap<>(); 100 | 101 | List annotated_classnames = new ClassGraph() 102 | .enableAnnotationInfo() 103 | .enableClassInfo() 104 | .acceptPackages(_package.orElse("")) 105 | .scan().getClassesWithAnnotation( _annotation.getName() ).getNames(); 106 | 107 | for(String classname: annotated_classnames) { 108 | 109 | Class clazz = null; 110 | 111 | try { 112 | clazz = Class.forName( classname ); 113 | } catch( ClassNotFoundException e ) { 114 | failures.put( classname, e ); 115 | continue; 116 | } catch( Throwable t ) { 117 | // probably BS static initialization; hope you don't need to mock this class... 118 | failures.put( classname, t ); 119 | continue; 120 | } 121 | 122 | if( _supertype.isAssignableFrom( clazz ) ) { 123 | annotated_classes.add( clazz ); 124 | } 125 | } 126 | 127 | if( failures.size() > 0 ) { 128 | if( "true".equals( System.getProperty( "PipelineExtensionDetector.expandFailures" ) ) ) { 129 | for( Entry failure : failures.entrySet() ) { 130 | LOG.error( failure.getKey(), failure.getValue() ); 131 | } 132 | } else { 133 | LOG.warn( 134 | "Failed to get some classes annotated with [{}] of type [{}] in package [{}]. For detailed error messages, set the system property PipelineExtensionDetector.expandFailures=true. Failures: [{}]", 135 | _annotation, 136 | _supertype, 137 | _package, 138 | failures.keySet() ); 139 | } 140 | } 141 | 142 | return annotated_classes; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/ArgumentCaptureSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing; 19 | 20 | /** 21 | * Verifies the idioms for capturing arguments delivered to mocked pipeline steps. 22 | * Steps with multiple arguments must have the captured arguments unwrapped once. 23 | * See spockframework/spock issue #603. 24 | * 25 | * @author awitt 26 | * 27 | */ 28 | public class ArgumentCaptureSpec extends JenkinsPipelineSpecification { 29 | 30 | def "single-argument capture" () { 31 | when: 32 | getPipelineMock("echo")("hello") 33 | then: 34 | 1 * getPipelineMock("echo")(_) >> { _arguments -> 35 | assert "hello" == _arguments[0] 36 | } 37 | } 38 | 39 | def "multi-argument capture" () { 40 | when: 41 | getPipelineMock("stage")("label") { 42 | echo("body") 43 | } 44 | then: 45 | 1 * getPipelineMock("stage")(_) >> { _arguments -> 46 | def args = _arguments[0] 47 | assert "label" == args[0] 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/DescriptorTimeJenkinsAccessingStepSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing; 19 | 20 | import javax.servlet.ServletContext 21 | 22 | import org.jvnet.hudson.reactor.ReactorException 23 | 24 | import jenkins.model.Jenkins 25 | 26 | /** 27 | * Verifies that jenkins-spock can run specifications when a Jenkins {@link Extension}'s {@link Descriptor} 28 | * tries to access the Jenkins singleton instance in the Descriptor's constructor. 29 | * 30 | * @author awitt 31 | * @since 2.1.4 32 | * 33 | */ 34 | public class DescriptorTimeJenkinsAccessingStepSpec extends JenkinsPipelineSpecification { 35 | 36 | /** 37 | * Nothing actually needs to happen in this test. 38 | * The "test" is whether jenkins-spock can even get to the point where it can try to run this test, 39 | * because the jenkins-spock behavior being verified happens in jenkins-spock's setupSpec() method. 40 | */ 41 | def "test specifications can run when a Jenkins extension Descriptor tries to access the Jenkins singleton instance"() { 42 | when: 43 | true 44 | then: 45 | true 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/DescriptorTimeJenkinsInteractingSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing; 19 | 20 | import org.mockito.Mockito 21 | 22 | import hudson.model.Item 23 | import jenkins.model.Jenkins 24 | 25 | /** 26 | * Verifies that jenkins-spock can run specifications when a Jenkins {@link Extension}'s {@link Descriptor} 27 | * tries to access the Jenkins singleton instance in the Descriptor's constructor, 28 | * then tries to interact with the result of calling a method on that singleton. 29 | * This requires that the test-suite has successfully stubbed the interaction with Jenkins that happens 30 | * outside of normal Spock test cases. 31 | * 32 | * @author awitt 33 | * @since 2.1.4 34 | * 35 | */ 36 | public class DescriptorTimeJenkinsInteractingSpec extends JenkinsPipelineSpecification { 37 | 38 | static { 39 | /* Even though setupSpec() happens "once, before all tests" as if it were static, 40 | * it's still actually an instance method. 41 | * Therefore, the constructor of the test specification must be called before that time 42 | * which is as good a time as any to set up the system property that will activate the 43 | * problematic behavior that this test case verifies jenkins-spock can handle. 44 | * 45 | * Unfortunately, constructors are not allowed in Spock specifications. 46 | * So, we'll use static {} setup to ensure that the property is set 47 | * before any jenkins-spock code tries to run. 48 | */ 49 | 50 | System.setProperty( "jenkins.test-access.descriptor", "true" ) 51 | } 52 | 53 | /** 54 | * If we do not successfully stub {@link Jenkins#getAllItems()} to return non-null, 55 | * jenkins-spock won't be able to even load all of the Jenkins extensions in preparation 56 | * for this test suite. 57 | */ 58 | protected Jenkins makeStaticJenkins() { 59 | 60 | // Ridiculous: we're bringing Mockito into Spock so that we can mock a class outside of a Spock specification 61 | Jenkins jenkins = Mockito.mock( Jenkins.class ); 62 | Mockito.when( jenkins.getAllItems() ) 63 | .thenReturn( new ArrayList() ); 64 | 65 | return jenkins; 66 | 67 | } 68 | 69 | def cleanupSpec() { 70 | System.setProperty( "jenkins.test-access.descriptor", "false" ) 71 | } 72 | 73 | /** 74 | * Nothing actually needs to happen in this test. 75 | * The "test" is whether jenkins-spock can even get to the point where it can try to run this test, 76 | * because the jenkins-spock behavior being verified happens in jenkins-spock's setupSpec() method. 77 | */ 78 | def "test specifications can run when a Jenkins extension Descriptor tries to work with the results of method calls on the Jenkins singleton instance"() { 79 | when: 80 | true 81 | then: 82 | true 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/ParallelClosureExecutionSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing; 19 | 20 | /** 21 | * Verifies that the parallel(...) pipeline step is special-cased and 22 | * that any closures passed to it as arguments are executed. 23 | * 24 | * @author awitt 25 | * 26 | */ 27 | public class ParallelClosureExecutionSpec extends JenkinsPipelineSpecification { 28 | 29 | def "parallel() closures are executed when there is only one closure"() { 30 | when: 31 | parallel( 32 | "stream1": { 33 | echo( "stream1" ) 34 | } 35 | ) 36 | then: 37 | 1 * getPipelineMock("echo")("stream1") 38 | } 39 | 40 | def "parallel() closures are executed when there are multiple closures"() { 41 | when: 42 | parallel( 43 | "stream1": { 44 | echo( "stream1" ) 45 | }, 46 | "stream2": { 47 | echo( "stream2" ) 48 | } 49 | ) 50 | then: 51 | 1 * getPipelineMock("echo")("stream1") 52 | 1 * getPipelineMock("echo")("stream2") 53 | } 54 | 55 | def "parallel() does not error when no closures are provided"() { 56 | when: 57 | parallel() 58 | then: 59 | 0 * getPipelineMock("echo")(_) 60 | } 61 | 62 | def "parallel() handles non-closure arguments"() { 63 | when: 64 | parallel( 65 | "failFast": true, 66 | "stream 1" : { 67 | echo( "hello 1" ) 68 | }, 69 | "stream 2" : { 70 | echo( "hello 2" ) 71 | } 72 | ) 73 | then: 74 | 1 * getPipelineMock("echo")("hello 1") 75 | 1 * getPipelineMock("echo")("hello 2") 76 | } 77 | 78 | def "parallel() mock is called even when its closures are subsequently executed"() { 79 | when: 80 | parallel( 81 | "stream1": { 82 | echo( "stream1" ) 83 | }, 84 | "stream2": { 85 | echo( "stream2" ) 86 | } 87 | ) 88 | then: 89 | 1 * getPipelineMock("parallel")(_) 90 | } 91 | 92 | def "parallel() arguments can be captured"() { 93 | when: 94 | parallel( 95 | "stream1": { 96 | echo( "stream1" ) 97 | }, 98 | "stream2": { 99 | echo( "stream2" ) 100 | } 101 | ) 102 | then: 103 | 1 * getPipelineMock("parallel")(_ as Map) >> { _parallel_args -> 104 | Map parallel_map = _parallel_args[0] 105 | 106 | assert parallel_map instanceof Map 107 | assert parallel_map.size() == 2 108 | assert parallel_map.containsKey( "stream1" ) 109 | assert parallel_map.containsKey( "stream2" ) 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/TrailngClosureExecutionSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing; 19 | 20 | /** 21 | * Verifies that "trailng" closures are executed when passed to mocks. 22 | * It's overwhelmingly common for these to be executed that way in Jenkins pipelines, 23 | * so much so that it's reasonable for the framework to stub this behavior for all mocks. 24 | * 25 | * @author awitt 26 | * 27 | */ 28 | public class TrailingclosureExecutionSpec extends JenkinsPipelineSpecification { 29 | 30 | def setup() { 31 | explicitlyMockPipelineVariable("someSymbol") 32 | } 33 | 34 | def "STEP: trailing closure is executed when passed implicitly & is the only argument"() { 35 | when: 36 | node() { 37 | echo( "payload" ) 38 | } 39 | then: 40 | 1 * getPipelineMock("echo")("payload") 41 | } 42 | 43 | def "STEP: trailing closure is executed when passed implicitly & is not the only argument"() { 44 | when: 45 | node("label") { 46 | echo( "payload" ) 47 | } 48 | then: 49 | 1 * getPipelineMock("echo")("payload") 50 | } 51 | 52 | def "STEP: trailing closure is executed when passed explicitly & is the only argument"() { 53 | when: 54 | node({ echo("payload")}) 55 | then: 56 | 1 * getPipelineMock("echo")("payload") 57 | } 58 | 59 | def "STEP: trailing closure is executed when passed explicitly & is not the only argument"() { 60 | when: 61 | node("label", { echo("payload")}) 62 | then: 63 | 1 * getPipelineMock("echo")("payload") 64 | } 65 | 66 | def "SYMBOL: trailing closure is executed when passed implicitly & is the only argument"() { 67 | when: 68 | someSymbol() { 69 | echo( "payload" ) 70 | } 71 | then: 72 | 1 * getPipelineMock("echo")("payload") 73 | } 74 | 75 | def "SYMBOL: trailing closure is executed when passed implicitly & is not the only argument"() { 76 | when: 77 | someSymbol("label") { 78 | echo( "payload" ) 79 | } 80 | then: 81 | 1 * getPipelineMock("echo")("payload") 82 | } 83 | 84 | def "SYMBOL: trailing closure is executed when passed explicitly & is the only argument"() { 85 | when: 86 | someSymbol({ echo("payload")}) 87 | then: 88 | 1 * getPipelineMock("echo")("payload") 89 | } 90 | 91 | def "SYMBOL: trailing closure is executed when passed explicitly & is not the only argument"() { 92 | when: 93 | someSymbol("label", { echo("payload")}) 94 | then: 95 | 1 * getPipelineMock("echo")("payload") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/functions/ClassToTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.functions 19 | 20 | /** 21 | * A regular Groovy class that calls pipeline steps. 22 | * @author awitt 23 | * 24 | */ 25 | class ClassToTest { 26 | public Map helloNode(String _label, Closure _body) { 27 | return node( _label ) { 28 | echo( "Hello from a [${_label}] node!" ) 29 | _body() 30 | echo( "Goodbye from a [${_label}] node!" ) 31 | } 32 | } 33 | 34 | public methodCall() { 35 | echo( "called methodCall" ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/functions/ClassToTestSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.functions 19 | 20 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 21 | 22 | /** 23 | * Given a regular Groovy class that tries to call pipeline steps (here {@link ClassToTest}), 24 | * verify that those calls can be mocked. 25 | * 26 | * Further verify that calls that can't be dispatched to a mock are handled correctly. 27 | * 28 | * @author awitt 29 | * 30 | */ 31 | public class ClassToTestSpec extends JenkinsPipelineSpecification { 32 | def "helloNode" () { 33 | given: 34 | ClassToTest myVar = new ClassToTest() 35 | when: 36 | myVar.helloNode( "nodeType" ) { 37 | echo( "inside node" ) 38 | } 39 | then: 40 | 1 * getPipelineMock( "node" )("nodeType", _) 41 | 1 * getPipelineMock( "echo" )("Hello from a [nodeType] node!") 42 | 1 * getPipelineMock( "echo" )("Goodbye from a [nodeType] node!") 43 | 1 * getPipelineMock( "echo" )("inside node") 44 | } 45 | 46 | def "calling method with correct parameters"() { 47 | given: 48 | ClassToTest myVar = new ClassToTest() 49 | when: 50 | myVar.methodCall() 51 | then: 52 | 1 * getPipelineMock( "echo")( "called methodCall" ) 53 | } 54 | 55 | def "calling method with incorrect parameters" () { 56 | given: 57 | ClassToTest myVar = new ClassToTest() 58 | when: 59 | myVar.methodCall( "incorrectParameter" ) 60 | then: 61 | thrown MissingMethodException 62 | } 63 | 64 | def "calling nonexistent method"() { 65 | given: 66 | ClassToTest myVar = new ClassToTest() 67 | when: 68 | myVar.nonexistentMethod() 69 | then: 70 | thrown IllegalStateException 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/functions/ClassWithMissingHandlers.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.functions 19 | 20 | /** 21 | * A regular Groovy class that also implements methodMissing and propertyMissing handlers. 22 | * 23 | * @author awitt 24 | * 25 | */ 26 | class ClassWithMissingHandlers { 27 | 28 | public Map helloNode(String _label, Closure _body) { 29 | return node( _label ) { 30 | echo( "Hello from a [${_label}] node!" ) 31 | _body() 32 | echo( "Goodbye from a [${_label}] node!" ) 33 | } 34 | } 35 | 36 | def methodMissing(String _name, _args) { 37 | 38 | if( _name == "sh" ) { 39 | return "myCustomShReturnValue" 40 | } 41 | 42 | throw new MissingMethodException( _name, getClass(), _args ) 43 | } 44 | 45 | def propertyMissing(String _name) { 46 | 47 | if( _name == "dynamicProperty" ) { 48 | return "myCustomDynamicPropertyValue" 49 | } 50 | 51 | throw new MissingPropertyException( _name, getClass() ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/functions/ClassWithMissingHandlersSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.functions 19 | 20 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 21 | import com.homeaway.devtools.jenkins.testing.PipelineVariableImpersonator 22 | 23 | /** 24 | * When a class-under-test defines its own methodMissing or propertyMissing 25 | * methods, verify {@link JenkinsPipelineSpecification}'s best-effort attempt to delegate 26 | * to those handlers while still enabling mock pipeline steps and variables to be accessed 27 | * 28 | * @author awitt 29 | * 30 | */ 31 | public class ClassWithMissingHandlersSpec extends JenkinsPipelineSpecification { 32 | 33 | ClassWithMissingHandlers clazz 34 | 35 | def setup() { 36 | clazz = new ClassWithMissingHandlers() 37 | } 38 | 39 | def "normal methods work normally" () { 40 | when: 41 | clazz.helloNode( "nodeType" ) { 42 | echo( "inside node" ) 43 | } 44 | then: 45 | 1 * getPipelineMock( "node" )("nodeType", _) 46 | 1 * getPipelineMock( "echo" )("Hello from a [nodeType] node!") 47 | 1 * getPipelineMock( "echo" )("Goodbye from a [nodeType] node!") 48 | 1 * getPipelineMock( "echo" )("inside node") 49 | } 50 | 51 | def "truly missing methods raise exception" () { 52 | when: 53 | clazz.madeUpMethod( "foo" ) 54 | then: 55 | thrown IllegalStateException 56 | } 57 | 58 | def "missing pipeline steps hit the mock" () { 59 | when: 60 | clazz.stage("someStage") { 61 | "cats" 62 | } 63 | then: 64 | 1 * getPipelineMock("stage")( "someStage", _ ) 65 | } 66 | 67 | def "class' own methodMissing is preferred" () { 68 | given: 69 | def retval = clazz.sh( "foo" ) 70 | expect: 71 | "myCustomShReturnValue" == retval 72 | } 73 | 74 | def "truly missing properties raise exception" () { 75 | when: 76 | clazz.madeUpProperty 77 | then: 78 | thrown IllegalStateException 79 | } 80 | 81 | def "missing pipeline vars hit the mock" () { 82 | setup: 83 | explicitlyMockPipelineVariable( "DefinedGlobalVariable" ) 84 | def real_global_var = clazz.DefinedGlobalVariable 85 | expect: 86 | real_global_var != null 87 | real_global_var instanceof PipelineVariableImpersonator 88 | } 89 | 90 | def "class' own propertyMissing is preferred" () { 91 | given: 92 | def retval = clazz.dynamicProperty 93 | expect: 94 | "myCustomDynamicPropertyValue" == retval 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/functions/ClassWithNoMethods.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2019 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.functions 19 | 20 | /** 21 | * A class with no methods. 22 | * @author awitt 23 | * 24 | */ 25 | class ClassWithNoMethods {} 26 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/functions/ClassWithNoMethodsSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2019 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.functions 19 | 20 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 21 | 22 | /** 23 | * Verify that classes without methods don't trip up the "failed to dispatch a call" logic 24 | * 25 | * @author awitt 26 | * 27 | */ 28 | public class ClassWithNoMethodsSpec extends JenkinsPipelineSpecification { 29 | 30 | def "calling nonexistent method"() { 31 | given: 32 | ClassToTest myVar = new ClassToTest() 33 | when: 34 | myVar.nonexistentMethod() 35 | then: 36 | thrown IllegalStateException 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/jobs/JenkinsfileSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.jobs 19 | 20 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 21 | 22 | /** 23 | * Given a classpath resource that is a whole pipeline script, 24 | * verify that {@link JenkinsPipelineSpecification#loadPipelineScriptForTest(java.lang.String)} 25 | * can successfully load that script and instrument its code to hit the mocks. 26 | * 27 | * @author awitt 28 | * 29 | */ 30 | public class JenkinsfileSpec extends JenkinsPipelineSpecification { 31 | def "Jenkinsfile"() { 32 | setup: 33 | def Jenkinsfile = loadPipelineScriptForTest("com/homeaway/devtools/jenkins/testing/jobs/Jenkinsfile.groovy") 34 | getPipelineMock("node")(_ as String, _ as Closure ) >> { String _label, Closure _body -> 35 | _body() 36 | } 37 | when: 38 | Jenkinsfile.run() 39 | then: 40 | 1 * getPipelineMock("node")("legacy", _) 41 | 1 * getPipelineMock("echo")("hello world") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/properties/JenkinsfileWithExplicitlyDefinedPropertySpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.properties 19 | 20 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 21 | import com.homeaway.devtools.jenkins.testing.PipelineVariableImpersonator 22 | 23 | /** 24 | * Given a whole pipeline script that attempts to access a variable that has been explicitly mocked with 25 | * {@link JenkinsPipelineSpecification#explicitlyMockPipelineVariable(java.lang.String)}, verify 26 | * that a mock for that variable is successfully created and connected. 27 | * 28 | * @author awitt 29 | * 30 | */ 31 | public class JenkinsfileWithExplicitlyDefinedPropertySpec extends JenkinsPipelineSpecification { 32 | 33 | protected Script Jenkinsfile 34 | 35 | def setupSpec() { 36 | JenkinsPipelineSpecification.metaClass = null 37 | PipelineVariableImpersonator.metaClass = null 38 | } 39 | 40 | def setup() { 41 | 42 | // mock the variable "env" 43 | explicitlyMockPipelineVariable("env") 44 | 45 | // Load the Jenkinsfile 46 | Jenkinsfile = loadPipelineScriptForTest("com/homeaway/devtools/jenkins/testing/properties/JenkinsfileWithExplicitlyDefinedProperty.groovy") 47 | } 48 | 49 | def "variable.propertyAccess" () { 50 | when: 51 | Jenkinsfile.run() 52 | then: 53 | 1 * getPipelineMock("stage")("property access", _) 54 | 1 * getPipelineMock("env.getProperty")("someEnvVar") >> "EXPECTED ENVVAR VALUE" 55 | 1 * getPipelineMock("echo")("EXPECTED ENVVAR VALUE") 56 | } 57 | 58 | def "inline property accesses" () { 59 | when: 60 | String someEnvvar = env.someEnvvar 61 | String otherEnvvar = env.otherEnvvar 62 | then: 63 | 1 * getPipelineMock( "env.getProperty" )( "someEnvvar" ) 64 | 1 * getPipelineMock( "env.getProperty" )( "otherEnvvar" ) >> "expected" 65 | expect: 66 | "expected" == otherEnvvar 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/properties/JenkinsfileWithPropertySpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.properties 19 | 20 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 21 | import com.homeaway.devtools.jenkins.testing.PipelineVariableImpersonator 22 | 23 | /** 24 | * Given a whole pipeline script that attempts to access a variable from an extension point 25 | * (likely a {@link org.jenkinsci.Symbol Symbol} or {@link org.jenkinsci.plugins.workflow.cps.GlobalVariable GlobalVariable}), verify that the correct mock object 26 | * is present and connected. 27 | * 28 | * @author awitt 29 | * 30 | */ 31 | public class JenkinsfileWithPropertySpec extends JenkinsPipelineSpecification { 32 | 33 | protected Script Jenkinsfile 34 | 35 | def setupSpec() { 36 | JenkinsPipelineSpecification.metaClass = null 37 | PipelineVariableImpersonator.metaClass = null 38 | } 39 | 40 | def setup() { 41 | // Load the Jenkinsfile 42 | Jenkinsfile = loadPipelineScriptForTest("com/homeaway/devtools/jenkins/testing/properties/JenkinsfileWithProperty.groovy") 43 | } 44 | 45 | def "Jenkinsfile"() { 46 | when: 47 | Jenkinsfile.run() 48 | then: 49 | 1 * getPipelineMock("node")("legacy", _) 50 | 1 * getPipelineMock("echo")("the end") 51 | expect: 52 | null != mocks 53 | 54 | null != mocks.get("docker") 55 | null != getPipelineMock("docker") 56 | mocks.get("docker") instanceof PipelineVariableImpersonator 57 | } 58 | 59 | def "expected method invocation on global variable" () { 60 | when: 61 | Jenkinsfile.run() 62 | then: 63 | 1 * getPipelineMock("stage")("expected", _) 64 | 1 * getPipelineMock("docker.expectedInvocation")("expected") 65 | expect: 66 | null != mocks.get("docker.expectedInvocation") 67 | null != getPipelineMock("docker.expectedInvocation") 68 | mocks.get("docker.expectedInvocation") instanceof Closure 69 | "Mock for type 'Closure' named '(implicit-expected) getPipelineMock(\"docker.expectedInvocation\")'" == mocks.get("docker.expectedInvocation").toString() 70 | } 71 | 72 | def "unexpected method invocation on global variable" () { 73 | when: 74 | Jenkinsfile.run() 75 | then: 76 | 1 * getPipelineMock("stage")("unexpected", _) 77 | expect: 78 | null != mocks.get("docker.unexpectedInvocation") 79 | null != getPipelineMock("docker.unexpectedInvocation") 80 | mocks.get("docker.unexpectedInvocation") instanceof Closure 81 | "Mock for type 'Closure' named '(implicit-runtime) getPipelineMock(\"docker.unexpectedInvocation\")'" == mocks.get("docker.unexpectedInvocation").toString() 82 | } 83 | 84 | def "stubbed method invocation on global variable" () { 85 | when: 86 | Jenkinsfile.run() 87 | then: 88 | 1 * getPipelineMock("stage")("stubbed", _) 89 | 1 * getPipelineMock("docker.stubbedInvocation")("original input from script") >> "stubbed" 90 | 1 * getPipelineMock("docker.stubbedInvocation")("stubbed") 91 | expect: 92 | null != mocks.get("docker.stubbedInvocation") 93 | null != getPipelineMock("docker.stubbedInvocation") 94 | mocks.get("docker.stubbedInvocation") instanceof Closure 95 | "Mock for type 'Closure' named '(implicit-expected) getPipelineMock(\"docker.stubbedInvocation\")'" == mocks.get("docker.stubbedInvocation").toString() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/properties/JenkinsfileWithUndefinedPropertySpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.properties 19 | 20 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 21 | import com.homeaway.devtools.jenkins.testing.PipelineVariableImpersonator 22 | 23 | /** 24 | * Given a whole pipeline script that attempts to access a variable that is truly not defined, 25 | * verify that the appropriate exception is thrown. 26 | * 27 | * @author awitt 28 | * 29 | */ 30 | public class JenkinsfileWithUndefinedPropertySpec extends JenkinsPipelineSpecification { 31 | 32 | protected Script Jenkinsfile 33 | 34 | def setupSpec() { 35 | JenkinsPipelineSpecification.metaClass = null 36 | PipelineVariableImpersonator.metaClass = null 37 | } 38 | 39 | def setup() { 40 | // Load the Jenkinsfile 41 | Jenkinsfile = loadPipelineScriptForTest("com/homeaway/devtools/jenkins/testing/properties/JenkinsfileWithUndefinedProperty.groovy") 42 | } 43 | 44 | def "fails"() { 45 | when: 46 | Jenkinsfile.run() 47 | then: 48 | thrown IllegalStateException 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/groovy/com/homeaway/devtools/jenkins/testing/scripts/InvalidClassNameSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing.scripts; 19 | 20 | import com.homeaway.devtools.jenkins.testing.InvalidlyNamedScriptWrapper 21 | import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification 22 | 23 | /** 24 | * Groovy script contents can be in files with any name. 25 | * Unlike compiled classes, the names don't have to be valid JVM class names. 26 | * 27 | * Ensure that we can still test such scripts. 28 | * 29 | * @author awitt 30 | * @since 2.1.1 31 | */ 32 | public class InvalidClassNameSpec extends JenkinsPipelineSpecification { 33 | 34 | def "scripts whose names create invalid JVM class names are wrapped"() { 35 | setup: 36 | def a_script = loadPipelineScriptForTest( "com/homeaway/devtools/jenkins/testing/scripts/some-script.groovy" ) 37 | expect: 38 | InvalidlyNamedScriptWrapper.class == a_script.getClass() 39 | } 40 | 41 | def "scripts whose names create valid JVM class names are not wrapped"() { 42 | setup: 43 | def a_script = loadPipelineScriptForTest( "com/homeaway/devtools/jenkins/testing/scripts/some_script.groovy" ) 44 | expect: 45 | // Groovy generates subclasses of Script for each Script 46 | Script.class.isAssignableFrom( a_script.getClass() ) 47 | } 48 | 49 | def "can run scripts whose names create invalid JVM class names"() { 50 | setup: 51 | def a_script = loadPipelineScriptForTest( "com/homeaway/devtools/jenkins/testing/scripts/some-script.groovy" ) 52 | when: 53 | a_script.run() 54 | then: 55 | 1 * getPipelineMock( "echo" )( "hello" ) 56 | 1 * getPipelineMock( "echo" )( "helped" ) 57 | } 58 | 59 | def "can run methods in scripts whose names create invalid JVM class names"() { 60 | setup: 61 | def a_script = loadPipelineScriptForTest( "com/homeaway/devtools/jenkins/testing/scripts/some-script.groovy" ) 62 | when: 63 | a_script.helper_method() 64 | then: 65 | 1 * getPipelineMock( "echo" )( "helped" ) 66 | } 67 | 68 | def "can get global variables in scripts whose names create invalid JVM class names"() { 69 | setup: 70 | def a_script = loadPipelineScriptForTest( "com/homeaway/devtools/jenkins/testing/scripts/some-script.groovy" ) 71 | expect: 72 | "GLOBAL" == a_script.global_variable 73 | } 74 | 75 | def "can set script variables in scripts whose names create invalid JVM class names"() { 76 | setup: 77 | def a_script = loadPipelineScriptForTest( "com/homeaway/devtools/jenkins/testing/scripts/some-script.groovy" ) 78 | a_script.getBinding().setVariable("new_property", "expected") 79 | expect: 80 | "expected" == a_script.getBinding().getVariable("new_property") 81 | } 82 | 83 | } 84 | 85 | -------------------------------------------------------------------------------- /src/test/java/com/homeaway/devtools/jenkins/testing/DescriptorTimeJenkinsAccessingStep.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package com.homeaway.devtools.jenkins.testing; 19 | 20 | import java.util.HashSet; 21 | import java.util.List; 22 | import java.util.Set; 23 | 24 | import org.jenkinsci.plugins.workflow.steps.Step; 25 | import org.jenkinsci.plugins.workflow.steps.StepContext; 26 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 27 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 28 | 29 | import hudson.Extension; 30 | import hudson.model.Item; 31 | import jenkins.model.Jenkins; 32 | 33 | /** 34 | * A step that tries to call {@link Jenkins#getInstanceOrNull()} when its descriptor is instantiated. 35 | *

36 | * This means that a Jenkins must exist in order for code to even figure out the name of this step. 37 | * Any Jenkins-Spock specification should fail with this class on the classpath, unless Jenkins-Spock 38 | * can successfully handle the situation. 39 | *

40 | *

41 | * This step further tries to interact with a result of a method call on the Jenkins object, 42 | * if the system property jenkins.test-access.descriptor is equal to the String true. 43 | * This allows test cases in jenkins-spock to demonstrate techniques for stubbing interactions with 44 | * this static Jenkins, while allowing test-cases that aren't related to the static Jenkins to continue 45 | * without issue. 46 | *

47 | * 48 | * @author awitt 49 | * @since 2.1.4 50 | * 51 | */ 52 | @Extension 53 | public class DescriptorTimeJenkinsAccessingStep extends Step { 54 | 55 | @Extension 56 | public static class DescriptorImpl extends StepDescriptor { 57 | 58 | public DescriptorImpl() { 59 | Jenkins maybe_jenkins = Jenkins.getInstanceOrNull(); 60 | assert maybe_jenkins != null; 61 | 62 | maybe_jenkins = Jenkins.get(); 63 | assert maybe_jenkins != null; 64 | 65 | List items = maybe_jenkins.getAllItems(); 66 | 67 | // set this property during tests if you want this class to mimic 68 | // "problematic" Jenkins extensions and try to actually interact with 69 | // the Jenkins object before any test specifications run. 70 | if (System.getProperty("jenkins.test-access.descriptor", "false").equals("true")) { 71 | // a test suite must have successfully stubbed getAllItems() in order for this 72 | // assertion to pass 73 | assert items != null; 74 | } 75 | } 76 | 77 | @Override 78 | public Set> getRequiredContext() { 79 | return new HashSet<>(); 80 | } 81 | 82 | @Override 83 | public String getFunctionName() { 84 | return "DescriptorTimeJenkinsAccessingStep"; 85 | } 86 | 87 | } 88 | 89 | @Override 90 | public StepExecution start(StepContext context) throws Exception { 91 | throw new UnsupportedOperationException("Not implemented in test step."); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/test/resources/com/homeaway/devtools/jenkins/testing/jobs/Jenkinsfile.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | node( "legacy" ) { 19 | echo( "hello world" ) 20 | } 21 | -------------------------------------------------------------------------------- /src/test/resources/com/homeaway/devtools/jenkins/testing/properties/JenkinsfileWithExplicitlyDefinedProperty.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | node( "legacy" ) { 19 | 20 | stage( "property access" ) { 21 | String envvar = env.someEnvVar 22 | echo( envvar ) 23 | } 24 | 25 | } 26 | 27 | echo("the end") 28 | -------------------------------------------------------------------------------- /src/test/resources/com/homeaway/devtools/jenkins/testing/properties/JenkinsfileWithProperty.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | node( "legacy" ) { 19 | 20 | stage( "expected" ) { 21 | docker.expectedInvocation("expected") 22 | } 23 | 24 | stage( "unexpected" ) { 25 | docker.unexpectedInvocation("unexpected") 26 | } 27 | 28 | stage( "stubbed" ) { 29 | def stubbed = docker.stubbedInvocation("original input from script") 30 | docker.stubbedInvocation( stubbed ) 31 | } 32 | 33 | } 34 | 35 | echo("the end") 36 | -------------------------------------------------------------------------------- /src/test/resources/com/homeaway/devtools/jenkins/testing/properties/JenkinsfileWithUndefinedProperty.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | node( "legacy" ) { 19 | 20 | stage( "missing property" ) { 21 | MadeUpProperty.expectedInvocation("expected") 22 | } 23 | 24 | } 25 | 26 | echo("the end") 27 | -------------------------------------------------------------------------------- /src/test/resources/com/homeaway/devtools/jenkins/testing/scripts/some-script.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | def binding_variable="BINDING" 19 | 20 | @groovy.transform.Field 21 | def global_variable="GLOBAL" 22 | 23 | node { 24 | stage("greet") { 25 | echo "hello" 26 | helper_method() 27 | } 28 | } 29 | 30 | def helper_method() { 31 | echo "helped" 32 | } 33 | -------------------------------------------------------------------------------- /src/test/resources/com/homeaway/devtools/jenkins/testing/scripts/some_script.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020 Expedia Group. 3 | All rights reserved. http://www.homeaway.com 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | echo "The contents of a validly-named Script are not important" 19 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n 13 | 14 | 15 | 16 | 17 | 19 | ${logdir}/stdout.log 20 | 21 | ${logdir}/stdout.log.%i 22 | 23 | 1 24 | 10 25 | 26 | 28 | 20MB 29 | 30 | 31 | [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | --------------------------------------------------------------------------------