diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..1821ef6b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,66 @@ +name: Build + +on: + push: + branches: + - master + - refactor-2 + pull_request: + branches: + - master + - refactor-2 + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + # Use system Gradle to regenerate wrapper with correct version (e.g., Gradle 9) + - name: Create gradle wrapper + run: gradle wrapper --gradle-version 9.0.0 --distribution-type all + + - name: Grant execute permission for Gradle wrapper + run: chmod +x ./gradlew + + - name: Run tests with coverage + run: ./gradlew test jacocoTestReport + + - name: Upload test results + uses: actions/upload-artifact@v4 + with: + name: test-results + path: build/reports/tests + + - name: Upload code coverage report + uses: actions/upload-artifact@v4 + with: + name: code-coverage-report + path: build/reports/jacoco/test/html + + - name: Generate docs + run: ./gradlew groovydoc + + - name: Upload GroovyDoc + uses: actions/upload-artifact@v4 + with: + name: groovydoc + path: build/docs/groovydoc \ No newline at end of file diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml new file mode 100644 index 00000000..3fd84f68 --- /dev/null +++ b/.github/workflows/doc.yml @@ -0,0 +1,180 @@ +name: Documentation Tests + +on: [push, pull_request] + +jobs: + Doc: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install Graphviz + run: sudo apt-get install graphviz + + - name: Install dependencies + run: | + pip install -r requirements_doc.txt + + - name: Download PlantUML jar + run: curl -L -o plantuml.jar https://github.com/plantuml/plantuml/releases/download/v1.2024.7/plantuml-1.2024.7.jar + + - name: Build doc + run: | + cd doc && rm -rf build && make html + cd .. + + CheckDocs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install Graphviz + run: sudo apt-get install graphviz + + - name: Install dependencies + run: | + pip install -r requirements_doc.txt + + - name: Download PlantUML jar + run: curl -L -o plantuml.jar https://github.com/plantuml/plantuml/releases/download/v1.2024.7/plantuml-1.2024.7.jar + + - name: Check doc build + run: | + cd doc + rm -rf build + make html SPHINXOPTS="-W" + cd .. + + - name: Check doc coverage + run: | + cd doc + make coverage + cat build/coverage/python.txt + cat build/coverage/python.txt | wc -l | xargs -I % test % -eq 2 + cd .. + + # To add when the doc links are fixed + # - name: Check doc links + # run: | + # cd doc + # make linkcheck + # cd .. + + DeployMainDoc: + runs-on: ubuntu-latest + needs: [CheckDocs, Doc] + if: github.ref == 'refs/heads/refactor-2' + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install Graphviz + run: sudo apt-get install graphviz + + - name: Install dependencies + run: | + pip install -r requirements_doc.txt + + - name: Download PlantUML jar + run: curl -L -o plantuml.jar https://github.com/plantuml/plantuml/releases/download/v1.2024.7/plantuml-1.2024.7.jar + + - name: Build doc and release + run: | + cd doc && rm -rf build && make html + cd .. + + - name: Publish doc + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./doc/build/html + publish_branch: refactor-2 + destination_dir: docs + + DeployDevelopmentDoc: + runs-on: ubuntu-latest + needs: [CheckDocs, Doc] + # Only run on pull requests to refactor-2 and non-forks + if: github.event_name == 'pull_request' && github.base_ref == 'refactor-2' && ! github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install Graphviz + run: sudo apt-get install graphviz + + - name: Install dependencies + run: | + pip install -r requirements_doc.txt + + - name: Download PlantUML jar + run: curl -L -o plantuml.jar https://github.com/plantuml/plantuml/releases/download/v1.2024.7/plantuml-1.2024.7.jar + + - name: Build doc and release + run: | + export GIT_BRANCH=${{ github.head_ref }} + export DEV_BUILD=1 + cd doc && rm -rf build && make html + cd .. + + - name: Publish doc + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./doc/build/html + publish_branch: refactor-2 + destination_dir: prs/${{ github.head_ref }} + + Deploy: + runs-on: ubuntu-latest + needs: [Doc] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install Graphviz + run: sudo apt-get install graphviz + + - name: Install dependencies + run: | + pip install -r requirements_doc.txt + pip install setuptools wheel twine build + + - name: Download PlantUML jar + run: curl -L -o plantuml.jar https://github.com/plantuml/plantuml/releases/download/v1.2024.7/plantuml-1.2024.7.jar + + - name: Build doc and release + run: | + cd doc && rm -rf build && make html + cd .. + python -m build + + - name: Publish doc + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./doc/build/html + publish_branch: refactor-2 + destination_dir: docs diff --git a/README.md b/README.md index 5832c159..25bdb392 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # jenkins-shared-library -[![Documentation Status](https://readthedocs.org/projects/jenkins-shared-library/badge/?version=latest)](https://jenkins-shared-library.readthedocs.io/en/latest/?badge=latest) +[![Documentation](https://github.com/sdgtt/jenkins-shared-library/actions/workflows/pages/pages-build-deployment/badge.svg)](https://sdgtt.github.io/jenkins-shared-library/) +[![Build](https://github.com/sdgtt/jenkins-shared-library/actions/workflows/build.yml/badge.svg)](https://github.com/sdgtt/jenkins-shared-library/actions/workflows/build.yml) CI Shared Library diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..e8bc6dbb --- /dev/null +++ b/build.gradle @@ -0,0 +1,61 @@ +plugins { + id 'groovy' + id 'java' + id 'jacoco' +} + +repositories { + mavenCentral() + maven { + url 'https://repo.jenkins-ci.org/releases/' + url 'https://repo.jenkins-ci.org/public/' + } +} + +dependencies { + implementation 'org.codehaus.groovy:groovy-all:3.0.9' + implementation 'com.cloudbees:groovy-cps:1.24' + implementation 'org.jenkins-ci.main:jenkins-core:2.121.2' + implementation('org.jenkinsci.plugins:pipeline-model-definition:2.2214.vb_b_34b_2ea_9b_83') { + artifact { + extension = 'jar' + } + } + // testImplementation 'junit:junit:4.13.2' + // testImplementation 'org.mockito:mockito-core:3.11.2' + testImplementation 'com.lesfurets:jenkins-pipeline-unit:1.9' + testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0' + testImplementation 'org.spockframework:spock-junit4:2.0-groovy-3.0' + testImplementation 'cglib:cglib-nodep:3.3.0' + + +} + +sourceSets { + main { + groovy { + srcDirs = ['src', 'vars'] + } + } + test { + groovy { + srcDirs = ['test'] + } + } +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + html.required = true + } +} \ No newline at end of file diff --git a/doc/source/_static/.gitkeep b/doc/source/_static/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/doc/source/_templates/.gitkeep b/doc/source/_templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/doc/source/conf.py b/doc/source/conf.py index 87e4fe91..fef16d15 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -12,6 +12,7 @@ # import os import sys +import sphinx_rtd_theme sys.path.insert(0, os.path.abspath('.')) @@ -37,8 +38,15 @@ "sphinx_rtd_theme", "sphinx.ext.graphviz", "sphinx.ext.autosectionlabel", - "sphinx_toolbox.collapse" + "sphinx_toolbox.collapse", + "sphinx.ext.coverage", + "myst_parser", + "sphinxcontrib.mermaid", + "sphinx_remove_toctrees", + "sphinx_click", + "sphinxcontrib.plantuml", ] +plantuml = "java -jar ../../plantuml.jar" autosectionlabel_prefix_document = True # Add any paths that contain templates here, relative to this directory. @@ -55,7 +63,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_rtd_theme" +html_theme = "furo" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/doc/source/gauntlet.pu b/doc/source/gauntlet.pu new file mode 100644 index 00000000..f7281cf0 --- /dev/null +++ b/doc/source/gauntlet.pu @@ -0,0 +1,23 @@ +@startuml Gauntlet Hardware Pipeline +start +title Gauntlet Hardware Pipeline +:START(master); +If (Update Agents (nuc-01)) + :Query Node; +else () + :Update Agents (nuc-02); +endif +switch (Check Required Hardware (master)) + case () + :Setup Docker (nuc-01-pluto); + :Linux Tests (nuc-01-pluto); + case () + :Setup Docker (nuc-01-zed-fmcomms2); + :Linux Tests (nuc-02); + + case () + :Setup Docker (nuc-02-zc706-daq2); + :Linux Tests (nuc-02-zc706-daq2); +endswitch + :Collect Logs; +@enduml \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst index c4a49a94..9681e3f3 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -41,6 +41,7 @@ Required Packages nebula artifacts vagrant + node_setup Indices and tables ================== diff --git a/doc/source/jenkinsfile_conf.rst b/doc/source/jenkinsfile_conf.rst index 79475d55..9118cffc 100644 --- a/doc/source/jenkinsfile_conf.rst +++ b/doc/source/jenkinsfile_conf.rst @@ -12,9 +12,13 @@ First, we will discuss the bare minimum requirements or parts for the Jenkinfile .. code-block:: groovy + // Register the pipeline context lock(label: 'adgt_test_harness_boards'){ @Library('sdgtt-lib@jsl_updates') _  - + + // Register the pipeline context + registerContext(this) + //instantiate constructor method def harness = getGauntlet()      @@ -76,7 +80,15 @@ An example of extended usage of getGauntlet is shown below. This is helpful when Update Agents Libraries ^^^^^^^^^^^^^^^^^^^^^^^ -Next on the list is to update first the agents with the required library dependencies. This is done by simply calling the method update_agents. This is required to ensure that libraries are always up to date. +Since we now secured and have access to the library, the next step is to register the Jenkins pipeline context and instantiate the constructor method. + +First, we need to register the pipeline context so the shared library can access Jenkins-specific features: + +.. code-block:: groovy + + registerContext(this) + +Then we instantiate the getGauntlet constructor method. This method calls a map that holds all constants and data members that can be overridden when constructing. This imitates a constructor and defines an instance of a Consul object. All according to API. The simplest way to use this is to use the default values that it holds. .. code-block:: groovy @@ -162,7 +174,7 @@ An example of using this method is shown below. harness.set_env('nebula_repo','https://github.com/sdgtt/nebula.git') harness.set_env('nebula_branch','dev') - harness.set_env('libiio_branch','v0.21') + harness.set_env('libiio_branch','v0.25') harness.set_env('telemetry_repo','https://github.com/sdgtt/telemetry.git') harness.set_env('telemetry_branch','master') @@ -210,15 +222,15 @@ Example Jenkinfile @Library('sdgtt-lib@jsl_updates') _  def hdlBranch = "NA" def linuxBranch = "NA" - def bootPartitionBranch = "2019_r2" - def firmwareVersion = 'v0.32' + def bootPartitionBranch = "2023_r2" + def firmwareVersion = 'v0.35' def bootfile_source = 'artifactory'  def harness = getGauntlet(hdlBranch, linuxBranch, bootPartitionBranch, firmwareVersion, bootfile_source)      //udpate repos harness.set_env('nebula_repo','https://github.com/sdgtt/nebula.git') harness.set_env('nebula_branch','dev') - harness.set_env('libiio_branch','v0.21') + harness.set_env('libiio_branch','v0.25') harness.set_env('telemetry_repo','https://github.com/kimpaller/telemetry.git') harness.set_env('telemetry_branch','master')      diff --git a/doc/source/libad9361tests_workflow.pu b/doc/source/libad9361tests_workflow.pu new file mode 100644 index 00000000..df233599 --- /dev/null +++ b/doc/source/libad9361tests_workflow.pu @@ -0,0 +1,72 @@ +@startuml LibAD9361Tests Workflow +start +title Test libad9361 Stage Workflow +:Execute LibAD9361Tests stage; + +:Define supported boards list; +note right +def supported_boards = [ + zynq-zed-adv7511-ad9361-fmcomms2-3', + zynq-zc706-adv7511-ad9361-fmcomms5', + zynq-adrv9361-z7035-fmc', + zynq-zed-adv7511-ad9364-fmcomms4', + pluto'] +end note + +if (Board supported and branch available?) then (yes) + note right: supported_boards.contains(board) && gauntEnv.libad9361_iio_branch != null + + partition "Try Block" { + :Execute Test libad9361 stage; + :Get board IP configuration; + note right: def ip = nebula("update-config -s network-config -f dutip --board-name="+board) + + :Clone libad9361-iio repository; + note right: run_i('git clone -b '+ gauntEnv.libad9361_iio_branch + ' ' + gauntEnv.libad9361_iio_repo, true) + + :Enter libad9361-iio directory; + note right: dir('libad9361-iio') + + partition "Build Setup (within dir)" { + :Create build directory; + note right: sh 'mkdir build' + + :Enter build directory; + note right: dir('build') + + partition "CMake Build Process" { + :Configure with CMake; + note right: sh 'cmake ..' + + :Compile with Make; + note right: sh 'make' + + :Run CTest with URI; + note right: sh 'URI_AD9361="ip:'+ip+'" ctest -T test --no-compress-output -V' + } + } + } + + partition "Finally Block" { + :Process test results; + note right + dir('libad9361-iio/build') { + xunit([CTest( + deleteOutputFiles: true, + failIfNotNew: true, + pattern: 'Testing/**/*.xml', + skipNoTestFiles: false, + stopProcessingIfError: true + )]) + } + end note + } + +else (no) + :Skip board testing; + note right: println("LibAD9361Tests: Skipping board: "+board) +endif + +:Stage completed; +stop +@enduml \ No newline at end of file diff --git a/doc/source/linuxtests_workflow.pu b/doc/source/linuxtests_workflow.pu new file mode 100644 index 00000000..e987d33e --- /dev/null +++ b/doc/source/linuxtests_workflow.pu @@ -0,0 +1,90 @@ +@startuml LinuxTests Workflow +start +title LinuxTests Stage Workflow +:Execute LinuxTests stage; +:Initialize variables; +note right +failed_test = '' +drivers_count = 0 +missing_drivers = 0 +end note + +partition "Try Block" { + :Get board IP address; + note right: def ip = nebula('update-config network-config dutip --board-name='+board) + + partition "DMESG Check" { + :Check DMESG for errors; + note right: nebula("net.check-dmesg - - ip='" + ip + "' - - board-name=" + board) + if (DMESG check fails?) then (yes) + :Add failure to failed_test; + note right: failed_test = failed_test + "[dmesg check failed: ${ex.getMessage()}]" + endif + } + + partition "IIO Devices Check" { + :Check IIO devices; + note right: nebula('driver.check-iio-devices - - uri="ip:'+ip+'" - - board-name='+board, true, true, true) + if (IIO devices check fails?) then (yes) + :Add failure to failed_test; + :Parse missing devices; + :Write missing devices log; + :Set elastic field for missing drivers; + note right + failed_test += "[iio_devices check failed: ${ex.getMessage()}]" + missing_devs = Eval.me(ex.getMessage().split('\n').last().split('not found')[1].replaceAll("'\$","")) + missing_drivers = missing_devs.size() + writeFile(file: board+'_missing_devs.log', text: missing_devs.join(",")) + set_elastic_field(board, 'drivers_missing', missing_drivers.toString()) + end note + endif + } + + :Get drivers enumerated; + note right: nebula('update-config driver-config iio_device_names -b '+board, false, true, false) + + partition "Diagnostics Check" { + if (Board is not firmware board?) then (yes) + :Run network diagnostics; + note right: nebula("net.run-diagnostics - - ip='"+ip+"' - - board-name="+board, true, true, true) + :Archive diagnostic reports; + note right: archiveArtifacts artifacts: '*_diag_report.tar.bz2', followSymlinks: false, allowEmptyArchive: true + if (Diagnostics fails?) then (yes) + :Add failure to failed_test; + note right: failed_test = failed_test + " [diagnostics failed: ${ex.getMessage()}]" + endif + endif + } + + if (Any tests failed?) then (yes) + :Throw Linux Tests Failed exception; + note right: if(failed_test && !failed_test.allWhitespace) + endif +} + +partition "Exception Handling" { + if (Exception occurs?) then (yes) + :Throw NominalException; + note right: throw new NominalException(ex.getMessage()) + endif +} + +partition "Finally Block" { + :Count DMESG errors and warnings; + note right + set_elastic_field(board, 'dmesg_errs', sh(returnStdout: true, script: 'cat dmesg_err_filtered.log | wc -l').trim()) + set_elastic_field(board, 'dmesg_warns', sh(returnStdout: true, script: 'cat dmesg_warn.log | wc -l').trim()) + end note + + :Rename and archive logs; + note right + run_i("if [ -f dmesg.log ]; then mv dmesg.log dmesg_" + board + ".log; fi") + run_i("if [ -f dmesg_err_filtered.log ]; then mv dmesg_err_filtered.log dmesg_" + board + "_err.log; fi") + run_i("if [ -f dmesg_warn.log ]; then mv dmesg_warn.log dmesg_" + board + "_warn.log; fi") + archiveArtifacts artifacts: '*.log', followSymlinks: false, allowEmptyArchive: true + end note +} + +:Stage completed; +stop +@enduml \ No newline at end of file diff --git a/doc/source/matlabtests_workflow.pu b/doc/source/matlabtests_workflow.pu new file mode 100644 index 00000000..51e2a8a1 --- /dev/null +++ b/doc/source/matlabtests_workflow.pu @@ -0,0 +1,84 @@ +@startuml MATLABTests Workflow +start +title Run MATLAB Toolbox Tests Stage Workflow +:Execute MATLABTests stage; + +:Initialize under_scm variable; +note right: def under_scm = true + +:Execute Run MATLAB Toolbox Tests stage; +note right: stage("Run MATLAB Toolbox Tests") + +:Get board IP configuration; +note right: def ip = nebula('update-config network-config dutip --board-name='+board) + +:Setup MATLAB environment; +note right: sh 'cp -r /root/.matlabro /root/.matlab' + +:Check pipeline type and reassign under_scm; +note right: under_scm = isMultiBranchPipeline() + +if (isMultiBranchPipeline()) then (yes) + partition "Multibranch Pipeline Path" { + :Checkout SCM with retry; + note right + retry(3) { + sleep(5) + checkout scm + sh 'git submodule update --init' + } + end note + + :Create MATLAB file; + note right: createMFile() + + partition "Try Block" { + :Run MATLAB tests; + note right + sh 'IIO_URI="ip:'+ip+'" board="'+board+'" elasticserver='+gauntEnv.elastic_server+' + /usr/local/MATLAB/'+gauntEnv.matlab_release+'/bin/matlab + -nosplash -nodesktop -nodisplay -r "run(\'matlab_commands.m\');exit"' + end note + } + + partition "Finally Block" { + :Archive JUnit results; + note right: junit testResults: '*.xml', allowEmptyResults: true + } + } + +else (no) + partition "Standard Pipeline Path" { + :Clone MATLAB repository; + note right + sh 'git clone --recursive -b '+gauntEnv.matlab_branch+' + '+gauntEnv.matlab_repo+' Toolbox' + end note + + :Enter Toolbox directory; + note right: dir('Toolbox') + + partition "Toolbox Setup (within dir)" { + :Create MATLAB file; + note right: createMFile() + + partition "Try Block" { + :Run MATLAB tests; + note right + sh 'IIO_URI="ip:'+ip+'" board="'+board+'" elasticserver='+gauntEnv.elastic_server+' + /usr/local/MATLAB/'+gauntEnv.matlab_release+'/bin/matlab + -nosplash -nodesktop -nodisplay -r "run(\'matlab_commands.m\');exit"' + end note + } + + partition "Finally Block" { + :Archive JUnit results; + note right: junit testResults: '*.xml', allowEmptyResults: true + } + } + } +endif + +:Stage completed; +stop +@enduml \ No newline at end of file diff --git a/doc/source/methods.rst b/doc/source/methods.rst index 019821f5..a1e37148 100644 --- a/doc/source/methods.rst +++ b/doc/source/methods.rst @@ -170,7 +170,7 @@ Sample usage: harness.set_env('nebula_repo','https://github.com/sdgtt/nebula.git') harness.set_env('nebula_branch','dev') - harness.set_env('libiio_branch','v0.21') + harness.set_env('libiio_branch','v0.25') harness.set_env('telemetry_repo','https://github.com/sdgtt/telemetry.git') harness.set_env('telemetry_branch','master') @@ -362,7 +362,7 @@ Sample usage: .. code-block:: groovy harness = getGauntlet() - harness.set_elastic_server('192.168.2.1') + harness.set_elastic_server('192.168.10.1') set_send_telemetry ^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/nebula.rst b/doc/source/nebula.rst index b81882cb..5c98954d 100644 --- a/doc/source/nebula.rst +++ b/doc/source/nebula.rst @@ -7,7 +7,7 @@ What is Nebula? To aid in development board management and interfacing the Nebula python tool is used. To know more about this tool, visit: `Nebula`_ -.. _Nebula: https://nebula-fpga-dev.readthedocs.io/en/latest/?badge=latest +.. _Nebula: https://sdgtt.github.io/nebula/main/flow.html Using Nebula in Jenkins Shared Library -------------------------------------- diff --git a/doc/source/nebula_workflow.pu b/doc/source/nebula_workflow.pu new file mode 100644 index 00000000..12737be7 --- /dev/null +++ b/doc/source/nebula_workflow.pu @@ -0,0 +1,16 @@ +@startuml Nebula Workflow +!theme plain + +participant "Jenkins Pipeline" as JP +participant "Shared Library" as SL +participant "Nebula Tool" as NT +participant "Development Board" as DB + +JP -> SL: Call nebula() method +SL -> NT: Execute nebula command +NT -> DB: Communicate with board +DB -> NT: Return response +NT -> SL: Command result +SL -> JP: Return output + +@enduml \ No newline at end of file diff --git a/doc/source/pipeline.rst b/doc/source/pipeline.rst index ac0a542e..d7b664af 100644 --- a/doc/source/pipeline.rst +++ b/doc/source/pipeline.rst @@ -4,7 +4,7 @@ Gauntlet Hardware Pipeline The main purpose of this shared library is to provide a standardize pipeline for software project that want to run code against hardware targets. The figure below shows an example pipeline that is generated using the Jenkinsfile below it. -.. graphviz:: pipeline_ex.dot +.. uml:: gauntlet.pu These pipelines have 3 main phases. Starting from the left side of the pipeline, phase 1 is the first 3 horizontal stages. In the first stage each agent, tools required are updated. Then for the next stage, each agent is queried to determined available hardware. This information is returned to the master node and the necessary downstream stages are determined. This will occur in all pipeline configuration using the **Gaunlet** class. The generated downstream stages will be based on how the **harness** object is configured and each of these downstream stages are run in a docker container, thus the Setup Docker stage which is the start of phase 2. diff --git a/doc/source/pyaditests_workflow.pu b/doc/source/pyaditests_workflow.pu new file mode 100644 index 00000000..167ccd8d --- /dev/null +++ b/doc/source/pyaditests_workflow.pu @@ -0,0 +1,137 @@ +@startuml PyADITests Workflow +start +title Run Python Tests Stage Workflow +:Execute Run Python Tests stage; + +partition "Try Block" { + :Get board configuration and declare variables; + note right + ip = nebula('update-config network-config dutip - - board-name=' + board) + serial = nebula('update-config uart-config address - - board-name=' + board) + def uri; + println('IP: ' + ip) + end note + + :Clone pytest-libiio repository; + note right + run_i('git clone -b "' + gauntEnv.pytest_libiio_branch + '" ' + gauntEnv.pytest_libiio_repo, true) + end note + + partition "pytest-libiio Setup" { + :Install pytest-libiio in its directory; + note right + dir('pytest-libiio') { + run_i('python3 setup.py install', true) + } + end note + } + + :Clone pyadi-iio repository; + note right + run_i('git clone -b "' + gauntEnv.pyadi_iio_branch + '" ' + gauntEnv.pyadi_iio_repo, true) + end note + + :Enter pyadi-iio directory; + note right: dir('pyadi-iio') + + partition "pyadi-iio Setup (within dir)" { + + :Install dependencies; + note right + run_i('pip3 install -r requirements.txt', true) + run_i('pip3 install -r requirements_dev.txt', true) + run_i('pip3 install pylibiio', true) + end note + + :Create test directories; + note right + run_i('mkdir testxml') + run_i('mkdir testhtml') + end note + + :Determine URI source; + if (iio_uri_source == "ip"?) then (yes) + :Set IP URI; + note right: uri = "ip:" + ip + else (no) + :Set serial URI; + note right: uri = "serial:" + serial + "," + gauntEnv.iio_uri_baudrate.toString() + endif + + :Configure board markers; + note right + check = check_for_marker(board) + board = board.replaceAll('-', '_') + board_name = check.board_name.replaceAll('-', '_') + marker = check.marker + end note + + :Build pytest command; + note right + cmd = "python3 -m pytest - - html=testhtml/report.html - - junitxml=testxml/" + board + "_reports.xml - - adi-hw-map -v -k 'not stress' -s - - uri='ip:"+ip+"' -m " + board_name + " - - capture=tee-sys" + marker + end note + + :Execute pytest tests; + note right: statusCode = sh script:cmd, returnStatus:true + + if (HTML report exists?) then (yes) + :Publish HTML report; + note right + publishHTML(target : [ + escapeUnderscores: false, + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'testhtml', + reportFiles: 'report.html', + reportName: board, + reportTitles: board]) + } + end note + endif + + if (XML results exist?) then (yes) + partition "Parse pytest results (within try-catch)" { + :Define pytest metrics to parse; + note right + def pytest_logs = ['errors', 'failures', 'skipped', 'tests'] + end note + + :Parse each metric from XML; + note right + pytest_logs.each { + cmd = 'cat testxml/' + board + '_reports.xml | sed -rn \'s/.*' + cmd+= it + '="([0-9]+)".*/\\1/p\'' + set_elastic_field(board.replaceAll('_', '-'), it, sh(returnStdout: true, script: cmd).trim()) + } + end note + + if (Parsing succeeds?) then (no) + :Log parsing failure; + note right + catch(Exception ex){ + println('Parsing pytest results failed') + echo getStackTrace(ex) + } + end note + endif + } + endif + + :Evaluate pytest status code; + + if ((statusCode != 5) && (statusCode != 0)) then (yes) + :throw new NominalException('PyADITests Failed'); + else (no) + endif + } +} + +partition "Finally Block" { + :Archive test results; + note right: junit testResults: 'pyadi-iio/testxml/*.xml', allowEmptyResults: true +} + +:Stage completed; +stop +@enduml \ No newline at end of file diff --git a/doc/source/recoverboard_workflow.pu b/doc/source/recoverboard_workflow.pu new file mode 100644 index 00000000..629c0fba --- /dev/null +++ b/doc/source/recoverboard_workflow.pu @@ -0,0 +1,42 @@ +@startuml RecoverBoard Workflow +start +title RecoverBoard Stage Workflow +:Execute RecoverBoard stage; +:Define reference branches; +note right: ref_branch = ['boot_partition', 'release'] +if (Board is pluto?) then (yes) + :Log "Recover stage does not support pluto yet!"; + stop +else (no) + :Enter recovery directory; + partition "Try Block" { + :Fetch reference boot files; + note right: 'dl.bootfiles - -board-name=' + board + ' - -source-root="' + gauntEnv.nebula_local_fs_source_root + '" - -source=' + gauntEnv.bootfile_source + ' - -branch="' + ref_branch.toString() + '"' + :Extract reference fsbl and u-boot; + note right + cd outs/ + cp bootgen_sysfiles.tgz .. + tar -xzvf bootgen_sysfiles.tgz + cp u-boot-*.elf u-boot.elf + end note + :Execute board recovery; + note right: manager.recovery-device-manager ____board-name={board} ____folder=outs ____sdcard + } + partition "Exception Handling" { + if (Exception occurs?) then (yes) + :Log stack trace; + :Re-throw exception; + note right: throw ex + endif + } + partition "Finally Block" { + :Archive UART logs; + note right + run_i("if [ -f ${board}.log ]; then mv ${board}.log uart_recover_" + board + ".log; fi") + archiveArtifacts artifacts: 'uart_recover_*.log', followSymlinks: false, allowEmptyArchive: true + end note + } +endif +:Stage completed; +stop +@enduml \ No newline at end of file diff --git a/doc/source/sendresults_workflow.pu b/doc/source/sendresults_workflow.pu new file mode 100644 index 00000000..e1b846cf --- /dev/null +++ b/doc/source/sendresults_workflow.pu @@ -0,0 +1,72 @@ +@startuml SendResults Workflow +start +title SendResults Stage Workflow +:Execute SendResults stage; +:Initialize release flags; +note right +is_hdl_release = "False" +is_linux_release = "False" +is_boot_partition_release = "False" +end note + +if (bootPartitionBranch == 'NA'?) then (yes) + :Set HDL and Linux release flags; + note right + is_hdl_release = ( gauntEnv.hdlBranch == "release" )? "True": "False" + is_linux_release = ( gauntEnv.linuxBranch == "release" )? "True": "False" + end note +else (no) + :Set boot partition release flag; + note right + is_boot_partition_release = ( gauntEnv.bootPartitionBranch == "release" )? "True": "False" + end note +endif + +:Log elastic logs and start message; +:Build elastic command string; +note right +cmd = 'boot_folder_name ' + board +cmd += ' hdl_hash ' + '\'' + get_elastic_field(board, 'hdl_hash' , 'NA') + '\'' +cmd += ' linux_hash ' + '\'' + get_elastic_field(board, 'linux_hash' , 'NA') + '\'' +cmd += ' boot_partition_hash ' + '\'' + gauntEnv.boot_partition_hash + '\'' +cmd += ' hdl_branch ' + gauntEnv.hdlBranch +cmd += ' linux_branch ' + gauntEnv.linuxBranch +cmd += ' boot_partition_branch ' + gauntEnv.bootPartitionBranch +cmd += ' is_hdl_release ' + is_hdl_release +cmd += ' is_linux_release ' + is_linux_release +cmd += ' is_boot_partition_release ' + is_boot_partition_release +end note + +:Add boot status fields; +note right +cmd += ' uboot_reached ' + get_elastic_field(board, 'uboot_reached', 'False') +cmd += ' linux_prompt_reached ' + get_elastic_field(board, 'linux_prompt_reached', 'False') +cmd += ' drivers_enumerated ' + get_elastic_field(board, 'drivers_enumerated', '0') +cmd += ' drivers_missing ' + get_elastic_field(board, 'drivers_missing', '0') +cmd += ' dmesg_warnings_found ' + get_elastic_field(board, 'dmesg_warns' , '0') +cmd += ' dmesg_errors_found ' + get_elastic_field(board, 'dmesg_errs' , '0') +end note + +:Add Jenkins metadata; +note right +cmd += ' jenkins_build_number ' + env.BUILD_NUMBER +cmd += ' jenkins_project_name ' + env.JOB_NAME +cmd += ' jenkins_agent ' + env.NODE_NAME +cmd += ' jenkins_trigger ' + gauntEnv.job_trigger +end note + +:Add test results; +note right +cmd += ' pytest_errors ' + get_elastic_field(board, 'errors', '0') +cmd += ' pytest_failures ' + get_elastic_field(board, 'failures', '0') +cmd += ' pytest_skipped ' + get_elastic_field(board, 'skipped', '0') +cmd += ' pytest_tests ' + get_elastic_field(board, 'tests', '0') +cmd += ' last_failing_stage ' + get_elastic_field(board, 'last_failing_stage', 'NA') +cmd += ' last_failing_stage_failure ' + get_elastic_field(board, 'last_failing_stage_failure', 'NA') +end note + +:Send logs to Elastic Search; +note right: sendLogsToElastic(cmd) +:Stage completed; +stop +@enduml \ No newline at end of file diff --git a/doc/source/stages.rst b/doc/source/stages.rst index a397fb9e..11650ac6 100644 --- a/doc/source/stages.rst +++ b/doc/source/stages.rst @@ -10,171 +10,21 @@ UpdateBOOTFiles This stage downloads the needed files and then proceeds to update the files on the device’s SD card. After updating, the device reboots. For pluto and m2k, this stage downloads their firmware and then updates the device’s firmware. -.. collapse:: UpdateBOOTFiles Stage - - .. code-block:: groovy - - case 'UpdateBOOTFiles': - println('Added Stage UpdateBOOTFiles') - cls = { String board -> - try { - stage('Update BOOT Files') { - println("Board name passed: "+board) - println(gauntEnv.branches.toString()) - if (board=="pluto") - nebula('dl.bootfiles --board-name=' + board + ' --branch=' + gauntEnv.firmwareVersion + ' --firmware', true, true, true) - else - nebula('dl.bootfiles --board-name=' + board + ' --source-root="' + gauntEnv.nebula_local_fs_source_root + '" --source=' + gauntEnv.bootfile_source - + ' --branch="' + gauntEnv.branches.toString() + '"', true, true, true) - //get git sha properties of files - get_gitsha(board) - //update-boot-files - nebula('manager.update-boot-files --board-name=' + board + ' --folder=outs', true, true, true) - if (board=="pluto") - nebula('uart.set-local-nic-ip-from-usbdev --board-name=' + board) - set_elastic_field(board, 'uboot_reached', 'True') - set_elastic_field(board, 'kernel_started', 'True') - set_elastic_field(board, 'linux_prompt_reached', 'True') - set_elastic_field(board, 'post_boot_failure', 'False') - }} - catch(Exception ex) { - echo getStackTrace(ex) - if (ex.getMessage().contains('u-boot not reached')){ - set_elastic_field(board, 'uboot_reached', 'False') - set_elastic_field(board, 'kernel_started', 'False') - set_elastic_field(board, 'linux_prompt_reached', 'False') - }else if (ex.getMessage().contains('u-boot menu cannot boot kernel')){ - set_elastic_field(board, 'uboot_reached', 'True') - set_elastic_field(board, 'kernel_started', 'False') - set_elastic_field(board, 'linux_prompt_reached', 'False') - }else if (ex.getMessage().contains('Linux not fully booting')){ - set_elastic_field(board, 'uboot_reached', 'True') - set_elastic_field(board, 'kernel_started', 'True') - set_elastic_field(board, 'linux_prompt_reached', 'False') - }else if (ex.getMessage().contains('Linux is functional but Ethernet is broken after updating boot files') || - ex.getMessage().contains('SSH not working but ping does after updating boot files')){ - set_elastic_field(board, 'uboot_reached', 'True') - set_elastic_field(board, 'kernel_started', 'True') - set_elastic_field(board, 'linux_prompt_reached', 'True') - set_elastic_field(board, 'post_boot_failure', 'True') - }else{ - echo "Update BOOT Files unexpectedly failed. ${ex.getMessage()}" - } - get_gitsha(board) - // send logs to elastic - if (gauntEnv.send_results){ - set_elastic_field(board, 'last_failing_stage', 'UpdateBOOTFiles') - failing_msg = "'" + ex.getMessage().split('\n').last().replaceAll( /(['])/, '"') + "'" - set_elastic_field(board, 'last_failing_stage_failure', failing_msg) - stage_library('SendResults').call(board) - } - throw new Exception('UpdateBOOTFiles failed: '+ ex.getMessage()) - }finally{ - //archive uart logs - run_i("if [ -f ${board}.log ]; then mv ${board}.log uart_boot_" + board + ".log; fi") - archiveArtifacts artifacts: 'uart_boot_*.log', followSymlinks: false, allowEmptyArchive: true - } - }; - -| +.. uml:: updatebootfiles_workflow.pu RecoverBoard ------------ This stage enables users to recover boards when they can no longer be accessed. Reference files are first downloaded, then the board is recovered using the recovery device manager function of Nebula. -.. collapse:: RecoverBoard Stage - - .. code-block:: groovy - - cls = { String board -> - stage('RecoverBoard'){ - echo "Recovering ${board}" - def ref_branch = ['boot_partition', 'release'] - if (board=="pluto"){ - echo "Recover stage does not support pluto yet!" - }else{ - dir ('recovery'){ - try{ - echo "Fetching reference boot files" - nebula('dl.bootfiles --board-name=' + board + ' --source-root="' + gauntEnv.nebula_local_fs_source_root + '" --source=' + gauntEnv.bootfile_source - + ' --branch="' + ref_branch.toString() + '"') - echo "Extracting reference fsbl and u-boot" - dir('outs'){ - sh("cp bootgen_sysfiles.tgz ..") - } - sh("tar -xzvf bootgen_sysfiles.tgz; cp u-boot-*.elf u-boot.elf") - echo "Executing board recovery..." - nebula('manager.recovery-device-manager --board-name=' + board + ' --folder=outs' + ' --sdcard') - }catch(Exception ex){ - echo getStackTrace(ex) - throw ex - }finally{ - //archive uart logs - run_i("if [ -f ${board}.log ]; then mv ${board}.log uart_recover_" + board + ".log; fi") - archiveArtifacts artifacts: 'uart_recover_*.log', followSymlinks: false, allowEmptyArchive: true - } - } - } - } - }; - -| +.. uml:: recoverboard_workflow.pu SendResults ----------- This stage sends the collected results from all stages to the elastic server which will then be processed for easy viewing. -.. collapse:: SendResults Stage - - .. code-block:: groovy - - cls = { String board -> - stage('SendLogsToElastic') { - is_hdl_release = "False" - is_linux_release = "False" - is_boot_partition_release = "False" - if (gauntEnv.bootPartitionBranch == 'NA'){ - is_hdl_release = ( gauntEnv.hdlBranch == "release" )? "True": "False" - is_linux_release = ( gauntEnv.linuxBranch == "release" )? "True": "False" - }else{ - is_boot_partition_release = ( gauntEnv.bootPartitionBranch == "release" )? "True": "False" - } - println(gauntEnv.elastic_logs) - echo 'Starting send log to elastic search' - cmd = 'boot_folder_name ' + board - cmd += ' hdl_hash ' + '\'' + get_elastic_field(board, 'hdl_hash' , 'NA') + '\'' - cmd += ' linux_hash ' + '\'' + get_elastic_field(board, 'linux_hash' , 'NA') + '\'' - cmd += ' boot_partition_hash ' + '\'' + gauntEnv.boot_partition_hash + '\'' - cmd += ' hdl_branch ' + gauntEnv.hdlBranch - cmd += ' linux_branch ' + gauntEnv.linuxBranch - cmd += ' boot_partition_branch ' + gauntEnv.bootPartitionBranch - cmd += ' is_hdl_release ' + is_hdl_release - cmd += ' is_linux_release ' + is_linux_release - cmd += ' is_boot_partition_release ' + is_boot_partition_release - cmd += ' uboot_reached ' + get_elastic_field(board, 'uboot_reached', 'False') - cmd += ' linux_prompt_reached ' + get_elastic_field(board, 'linux_prompt_reached', 'False') - cmd += ' drivers_enumerated ' + get_elastic_field(board, 'drivers_enumerated', '0') - cmd += ' drivers_missing ' + get_elastic_field(board, 'drivers_missing', '0') - cmd += ' dmesg_warnings_found ' + get_elastic_field(board, 'dmesg_warns' , '0') - cmd += ' dmesg_errors_found ' + get_elastic_field(board, 'dmesg_errs' , '0') - // cmd +="jenkins_job_date datetime.datetime.now(), - cmd += ' jenkins_build_number ' + env.BUILD_NUMBER - cmd += ' jenkins_project_name ' + env.JOB_NAME - cmd += ' jenkins_agent ' + env.NODE_NAME - cmd += ' jenkins_trigger ' + gauntEnv.job_trigger - cmd += ' pytest_errors ' + get_elastic_field(board, 'errors', '0') - cmd += ' pytest_failures ' + get_elastic_field(board, 'failures', '0') - cmd += ' pytest_skipped ' + get_elastic_field(board, 'skipped', '0') - cmd += ' pytest_tests ' + get_elastic_field(board, 'tests', '0') - cmd += ' last_failing_stage ' + get_elastic_field(board, 'last_failing_stage', 'NA') - cmd += ' last_failing_stage_failure ' + get_elastic_field(board, 'last_failing_stage_failure', 'NA') - sendLogsToElastic(cmd) - } - }; - -| +.. uml:: sendresults_workflow.pu Test Stages ----------- @@ -186,250 +36,25 @@ LinuxTests This stage checks for dmesg errors, checks iio devices, and runs diagnostics on boards. -.. collapse:: LinuxTests Stage - - .. code-block:: groovy - - case 'LinuxTests': - println('Added Stage LinuxTests') - cls = { String board -> - stage('Linux Tests') { - def failed_test = '' - def drivers_count = 0 - def missing_drivers = 0 - try { - // run_i('pip3 install pylibiio',true) - //def ip = nebula('uart.get-ip') - def ip = nebula('update-config network-config dutip --board-name='+board) - try{ - nebula("net.check-dmesg --ip='"+ip+"' --board-name="+board) - }catch(Exception ex) { - failed_test = failed_test + "[dmesg check failed: ${ex.getMessage()}]" - } - - try{ - nebula('driver.check-iio-devices --uri="ip:'+ip+'" --board-name='+board, true, true, true) - }catch(Exception ex) { - failed_test = failed_test + "[iio_devices check failed: ${ex.getMessage()}]" - missing_devs = Eval.me(ex.getMessage().split('\n').last().split('not found')[1].replaceAll("'\$","")) - missing_drivers = missing_devs.size() - writeFile(file: board+'_missing_devs.log', text: missing_devs.join(",")) - set_elastic_field(board, 'drivers_missing', missing_drivers.toString()) - } - // get drivers enumerated - println(nebula('update-config driver-config iio_device_names -b '+board, false, true, false)) - - try{ - if (!gauntEnv.firmware_boards.contains(board)) - nebula("net.run-diagnostics --ip='"+ip+"' --board-name="+board, true, true, true) - archiveArtifacts artifacts: '*_diag_report.tar.bz2', followSymlinks: false, allowEmptyArchive: true - }catch(Exception ex) { - failed_test = failed_test + " [diagnostics failed: ${ex.getMessage()}]" - } - - if(failed_test && !failed_test.allWhitespace){ - throw new Exception("Linux Tests Failed: ${failed_test}") - } - }catch(Exception ex) { - throw new NominalException(ex.getMessage()) - }finally{ - // count dmesg errs and warns - set_elastic_field(board, 'dmesg_errs', sh(returnStdout: true, script: 'cat dmesg_err_filtered.log | wc -l').trim()) - set_elastic_field(board, 'dmesg_warns', sh(returnStdout: true, script: 'cat dmesg_warn.log | wc -l').trim()) - // Rename logs - run_i("if [ -f dmesg.log ]; then mv dmesg.log dmesg_" + board + ".log; fi") - run_i("if [ -f dmesg_err_filtered.log ]; then mv dmesg_err_filtered.log dmesg_" + board + "_err.log; fi") - run_i("if [ -f dmesg_warn.log ]; then mv dmesg_warn.log dmesg_" + board + "_warn.log; fi") - archiveArtifacts artifacts: '*.log', followSymlinks: false, allowEmptyArchive: true - } - } - }; - -| +.. uml:: linuxtests_workflow.pu PyADITests ^^^^^^^^^^ This stage runs the pyadi-iio test on the target board. -.. collapse:: PyADITests Stage - - .. code-block:: groovy - - case 'PyADITests': - cls = { String board -> - stage('Run Python Tests') { - try - { - //def ip = nebula('uart.get-ip') - def ip = nebula('update-config network-config dutip --board-name='+board) - def serial = nebula('update-config uart-config address --board-name='+board) - def uri; - println('IP: ' + ip) - // temporarily get pytest-libiio from another source - run_i('git clone -b "' + gauntEnv.pytest_libiio_branch + '" ' + gauntEnv.pytest_libiio_repo, true) - dir('pytest-libiio'){ - run_i('python3 setup.py install', true) - } - run_i('git clone -b "' + gauntEnv.pyadi_iio_branch + '" ' + gauntEnv.pyadi_iio_repo, true) - dir('pyadi-iio') - { - run_i('pip3 install -r requirements.txt', true) - run_i('pip3 install -r requirements_dev.txt', true) - run_i('pip3 install pylibiio', true) - run_i('mkdir testxml') - run_i('mkdir testhtml') - if (gauntEnv.iio_uri_source == "ip") - uri = "ip:" + ip; - else - uri = "serial:" + serial + "," + gauntEnv.iio_uri_baudrate.toString() - check = check_for_marker(board) - board = board.replaceAll('-', '_') - board_name = check.board_name.replaceAll('-', '_') - marker = check.marker - cmd = "python3 -m pytest --html=testhtml/report.html --junitxml=testxml/" + board + "_reports.xml --adi-hw-map -v -k 'not stress' -s --uri='ip:"+ip+"' -m " + board_name + " --capture=tee-sys" + marker - def statusCode = sh script:cmd, returnStatus:true - - // generate html report - if (fileExists('testhtml/report.html')){ - publishHTML(target : [ - escapeUnderscores: false, - allowMissing: false, - alwaysLinkToLastBuild: false, - keepAll: true, - reportDir: 'testhtml', - reportFiles: 'report.html', - reportName: board, - reportTitles: board]) - } - - // get pytest results for logging - if(fileExists('testxml/' + board + '_reports.xml')){ - try{ - def pytest_logs = ['errors', 'failures', 'skipped', 'tests'] - pytest_logs.each { - cmd = 'cat testxml/' + board + '_reports.xml | sed -rn \'s/.*' - cmd+= it + '="([0-9]+)".*/\\1/p\'' - set_elastic_field(board.replaceAll('_', '-'), it, sh(returnStdout: true, script: cmd).trim()) - } - // println(gauntEnv.elastic_logs[board.replaceAll('_', '-')]) - }catch(Exception ex){ - println('Parsing pytest results failed') - echo getStackTrace(ex) - } - } - - // throw exception if pytest failed - if ((statusCode != 5) && (statusCode != 0)){ - // Ignore error 5 which means no tests were run - throw new NominalException('PyADITests Failed') - } - } - } - finally - { - // archiveArtifacts artifacts: 'pyadi-iio/testxml/*.xml', followSymlinks: false, allowEmptyArchive: true - junit testResults: 'pyadi-iio/testxml/*.xml', allowEmptyResults: true - } - } - } - -| +.. uml:: pyaditests_workflow.pu LibAD9361Test ^^^^^^^^^^^^^ This stage runs the LibAD9361 tests available on the repository. -.. collapse:: LibAD9361Tests Stage - - .. code-block:: groovy - - case 'LibAD9361Tests': - cls = { String board -> - def supported_boards = ['zynq-zed-adv7511-ad9361-fmcomms2-3', - 'zynq-zc706-adv7511-ad9361-fmcomms5', - 'zynq-adrv9361-z7035-fmc', - 'zynq-zed-adv7511-ad9364-fmcomms4', - 'pluto'] - if(supported_boards.contains(board) && gauntEnv.libad9361_iio_branch != null){ - try{ - stage("Test libad9361") { - def ip = nebula("update-config -s network-config -f dutip --board-name="+board) - run_i('git clone -b '+ gauntEnv.libad9361_iio_branch + ' ' + gauntEnv.libad9361_iio_repo, true) - dir('libad9361-iio') - { - sh 'mkdir build' - dir('build') - { - sh 'cmake ..' - sh 'make' - sh 'URI_AD9361="ip:'+ip+'" ctest -T test --no-compress-output -V' - } - } - } - } - finally - { - dir('libad9361-iio/build'){ - xunit([CTest(deleteOutputFiles: true, failIfNotNew: true, pattern: 'Testing/**/*.xml', skipNoTestFiles: false, stopProcessingIfError: true)]) - } - } - }else{ - println("LibAD9361Tests: Skipping board: "+board) - } - } - break - default: - throw new Exception('Unknown library stage: ' + stage_name) - } - -| +.. uml:: libad9361tests_workflow.pu MATLABTests ^^^^^^^^^^^ This stage runs the MATLAB hardware test runner for the target boards. -.. collapse:: MATLABTests Stage - - .. code-block:: groovy - - case 'MATLABTests': - cls = { String board -> - def under_scm = true - stage("Run MATLAB Toolbox Tests") { - def ip = nebula('update-config network-config dutip --board-name='+board) - sh 'cp -r /root/.matlabro /root/.matlab' - under_scm = isMultiBranchPipeline() - if (under_scm) - { - println("Multibranch pipeline. Checkout scm.") - retry(3) { - sleep(5) - checkout scm - sh 'git submodule update --init' - } - createMFile() - try{ - sh 'IIO_URI="ip:'+ip+'" board="'+board+'" elasticserver='+gauntEnv.elastic_server+' /usr/local/MATLAB/'+gauntEnv.matlab_release+'/bin/matlab -nosplash -nodesktop -nodisplay -r "run(\'matlab_commands.m\');exit"' - }finally{ - junit testResults: '*.xml', allowEmptyResults: true - } - } - else - { - println("Not a multibranch pipeline. Cloning "+gauntEnv.matlab_branch+" branch from "+gauntEnv.matlab_repo) - sh 'git clone --recursive -b '+gauntEnv.matlab_branch+' '+gauntEnv.matlab_repo+' Toolbox' - dir('Toolbox') - { - createMFile() - try{ - sh 'IIO_URI="ip:'+ip+'" board="'+board+'" elasticserver='+gauntEnv.elastic_server+' /usr/local/MATLAB/'+gauntEnv.matlab_release+'/bin/matlab -nosplash -nodesktop -nodisplay -r "run(\'matlab_commands.m\');exit"' - }finally{ - junit testResults: '*.xml', allowEmptyResults: true - } - } - } - } - } +.. uml:: matlabtests_workflow.pu \ No newline at end of file diff --git a/doc/source/updatebootfiles_workflow.pu b/doc/source/updatebootfiles_workflow.pu new file mode 100644 index 00000000..2ceac114 --- /dev/null +++ b/doc/source/updatebootfiles_workflow.pu @@ -0,0 +1,127 @@ +@startuml UpdateBOOTFiles Workflow +start +title UpdateBOOTFiles Stage Workflow +:Execute UpdateBOOTFiles stage; +:Print board name and branches; + +partition "Try Block" { + if (Board is Pluto?) then (yes) + :Download firmware files via Nebula; + note right: dl.bootfiles ____board-name={board} ____branch={firmwareVersion} ____firmware + else (no) + :Download boot files via Nebula; + note right: dl.bootfiles ____board-name={board} ____source-root={source_root} ____source={source} ____branch={branches} + endif + :Get git SHA for board; + :Update boot files on device; + note right: manager.update-boot-files ____board-name={board} ____folder=outs + if (Board is Pluto?) then (yes) + :Set local NIC IP from USB device; + note right: uart.set-local-nic-ip-from-usbdev ____board-name={board} + endif + :Set elastic fields (success); + note right + set_elastic_field(board, 'uboot_reached', 'True') + set_elastic_field(board, 'kernel_started', 'True') + set_elastic_field(board, 'linux_prompt_reached', 'True') + set_elastic_field(board, 'post_boot_failure', 'False') + end note +} + +partition "Exception Handling" { + if (Exception occurs?) then (yes) + :Log stack trace; + switch (Exception type?) + case (u-boot not reached) + :Set elastic fields; + note right + board, uboot_reached = False + board, kernel_started = False + board, linux_prompt_reached = False + end note + case (u-boot menu cannot boot kernel) + :Set elastic fields; + note right + board, uboot_reached = True + board, kernel_started = False + board, linux_prompt_reached = False + end note + case (Linux not fully booting) + :Set elastic fields; + note right + board, uboot_reached = True + board, kernel_started = True + board, linux_prompt_reached = False + end note + case (Ethernet/SSH issues) + :Set elastic fields; + note right + board, uboot_reached = True + board, kernel_started = True + board, linux_prompt_reached = True + board, post_boot_failure = True + end note + case (Other errors) + :Log unexpected failure; + endswitch + :Get git SHA for board; + if (send_results enabled?) then (yes) + :Set failing stage info; + note right + last_failing_stage = 'UpdateBOOTFiles' + last_failing_stage_failure = exception message + end note + :Call SendResults stage; + endif + :Throw UpdateBOOTFiles failed exception; + note right: Exception thrown but finally block still executes + endif +} + +partition "Finally Block" { + :Archive UART logs; + note right + run_i("if [ -f ${board}.log ]; then mv ${board}.log uart_boot_" + board + ".log; fi") + archiveArtifacts artifacts: 'uart_boot_*.log', followSymlinks: false, allowEmptyArchive: true + end note +} + +stop +@enduml + Success Case: + - uboot_reached: True + - kernel_started: True + - linux_prompt_reached: True + - post_boot_failure: False +end note + +alt Exception occurs + UBF -> UBF: Analyze exception message + + alt u-boot not reached + UBF -> ES: Set uboot_reached=False, kernel_started=False, linux_prompt_reached=False + else u-boot menu cannot boot kernel + UBF -> ES: Set uboot_reached=True, kernel_started=False, linux_prompt_reached=False + else Linux not fully booting + UBF -> ES: Set uboot_reached=True, kernel_started=True, linux_prompt_reached=False + else Ethernet/SSH issues + UBF -> ES: Set uboot_reached=True, kernel_started=True, linux_prompt_reached=True, post_boot_failure=True + else Other errors + UBF -> UBF: Log unexpected failure + end + + UBF -> UBF: get_gitsha(board) + + alt send_results enabled + UBF -> ES: Set last_failing_stage=UpdateBOOTFiles + UBF -> ES: Set last_failing_stage_failure message + UBF -> UBF: Call SendResults stage + end + + UBF -> JP: Throw UpdateBOOTFiles failed exception +end + +UBF -> UBF: Archive UART logs (rename {board}.log to uart_boot_{board}.log) +UBF -> JP: Stage completed + +@enduml \ No newline at end of file diff --git a/doc/source/vagrant.rst b/doc/source/vagrant.rst index 9ba70f65..4ffb53c7 100644 --- a/doc/source/vagrant.rst +++ b/doc/source/vagrant.rst @@ -16,7 +16,7 @@ This function can be called by typing following code:: Using the ``vagrant box list`` command, the function checks for Vagrant box called "win" and return "true" if it exists or "false" if there is no box with this name. -Click `here `_ for code. +Click `here for check_for_box code `_. check_for_snapshot() -------------------- @@ -27,7 +27,7 @@ This function can be called by typing following code:: Using the ``vagrant snapshot list`` command, the function checks for Vagrant snapshot called "initial-state" and return "true" if it exists or "false" if there is no snapshot with this name. -Click `here `_ for code. +Click `here for check_for_snapshot code `_. check_node() ------------ @@ -41,7 +41,7 @@ This definition contains two functions: * ``get_agents()``: return names of all online Jenkins agents. * ``call()``: uses ``get_agents()`` to search for a Jenkins agent called "win-vm" and return the name or if nothing is found, function will display error logs. -Click `here `_ for code. +Click `here for check_node code `_. get_vagrant_vm_id() ------------------- @@ -52,7 +52,7 @@ This function can be called by typing following code:: Using the ``vagrant global-status`` command, this function searches for a Vagrant VM at a specific location/workspace and returns its ID or a message if there is no VM. -Click `here `_ for code. +Click `here for get_vagrant_vm_id code `_. run_closure() ------------- @@ -95,10 +95,13 @@ Let suppose that we have Vagrant VM at location "users/vagrant" and we want to r } -Click `here `_ for code. + } + } + +Click `here for run_closure code `_. setup_jenkins_agent_on_vagrant_vm() ---------------------- +------------------------------------ This function can be called by typing following code:: @@ -106,7 +109,7 @@ This function can be called by typing following code:: This will run ssh commands to install a Jenkins agent inside Vagrant VM. As an argument there is the IP of the server where Vagrant VM is located. -Click `here `_ for code. +Click `here for setup_jenkins_agent code `_. setup_vagrant_box() ------------------- @@ -119,12 +122,10 @@ This will run ssh commands to download and add a box and copy a Vagrantfile in o As arguments there are: -* boxaddress: HTTP URL to a box -* newboxname: a new name for the box to be added -* oldboxname: the name of the box in HTTP URL address -* vagrantfilepath: an absolute path to a Vagrantfile on the current server to be used for Vagrant VM +* path: path to place where Vagrant Box will be downloaded +* name: a name of the box -Click `here `_ for code. +Click `here for setup_vagrant_box code `_. End user example ---------------- diff --git a/requirements_doc.txt b/requirements_doc.txt index 14c3d9c5..69b98eb6 100644 --- a/requirements_doc.txt +++ b/requirements_doc.txt @@ -1,4 +1,13 @@ sphinx>=1.7.6 sphinx-rtd-theme>=0.4.0 sphinx-toolbox -graphviz \ No newline at end of file +graphviz +myst-parser +furo +sphinx-remove-toctrees +sphinx-favicon +sphinxcontrib-mermaid +sphinx-simplepdf +pillow +sphinx-click +sphinxcontrib-plantuml \ No newline at end of file diff --git a/src/sdg/Gauntlet.groovy b/src/sdg/Gauntlet.groovy index 703db723..20bde239 100644 --- a/src/sdg/Gauntlet.groovy +++ b/src/sdg/Gauntlet.groovy @@ -1,11 +1,24 @@ package sdg + import sdg.FailSafeWrapper import sdg.NominalException +import sdg.Logger +import sdg.ioc.* import org.jenkinsci.plugins.pipeline.modeldefinition.Utils +import com.cloudbees.groovy.cps.NonCPS /** A map that holds all constants and data members that can be override when constructing */ gauntEnv +/** context */ +isDefaultContext + +/** steps */ +stepExecutor + +/** logger */ +logger + /** * Imitates a constructor * Defines an instance of Consul object. All according to api @@ -18,8 +31,43 @@ gauntEnv */ def construct(hdlBranch, linuxBranch, bootPartitionBranch, firmwareVersion, bootfile_source) { // initialize gauntEnv - gauntEnv = getGauntEnv(hdlBranch, linuxBranch, bootPartitionBranch, firmwareVersion, bootfile_source) + isDefaultContext = ContextRegistry.getContext().isDefault() + stepExecutor = ContextRegistry.getContext().getStepExecutor() + logger = new Logger(this) + gauntEnv = stepExecutor.getGauntEnv(hdlBranch, linuxBranch, bootPartitionBranch, firmwareVersion, bootfile_source) gauntEnv.agents_online = getOnlineAgents() + if(isDefaultContext){ + gauntEnv.env = env + }else{ + gauntEnv.env = [:] + } +} + +// @NonCPS +def getOnlineAgents() { + + def online_agents = [] + if(!isDefaultContext){ + return online_agents + } + def jenkins = Jenkins.instance + for (agent in jenkins.getNodes()) { + def computer = agent.computer + if (computer.name == 'alpine') { + continue + } + if (!computer.offline) { + if (!gauntEnv.required_agent.isEmpty()){ + if (computer.name in gauntEnv.required_agent){ + online_agents.add(computer.name) + } + }else{ + online_agents.add(computer.name) + } + } + } + logger.info("Online agents: ${online_agents}") + return online_agents } /* * @@ -44,8 +92,8 @@ private def setup_agents() { stage('Query agents') { // Get necessary configuration for basic work if (gauntEnv.workspace == '') { - gauntEnv.workspace = env.WORKSPACE - gauntEnv.build_no = env.BUILD_NUMBER + gauntEnv.workspace = gauntEnv.env.WORKSPACE + gauntEnv.build_no = gauntEnv.env.BUILD_NUMBER } board = nebula('update-config board-config board-name -y ' + gauntEnv.nebula_config_path + '/' +agent_name) board_map[agent_name] = board @@ -84,8 +132,8 @@ private def update_agent() { node(agent_name) { // clean up residue containers and detached screen sessions stage('Clean up residue docker containers') { - sh 'sudo docker ps -q -f status=exited | xargs --no-run-if-empty sudo docker rm' - sh 'sudo screen -ls | grep Detached | cut -d. -f1 | awk "{print $1}" | sudo xargs -r kill' //close all detached screen session on the agent + stepExecutor.sh 'sudo docker ps -q -f status=exited | xargs --no-run-if-empty sudo docker rm' + stepExecutor.sh 'sudo screen -ls | grep Detached | cut -d. -f1 | awk "{print $1}" | sudo xargs -r kill' //close all detached screen session on the agent cleanWs() } // automatically update nebula config @@ -97,7 +145,7 @@ private def update_agent() { } if(gauntEnv.update_nebula_config){ stage('Update Nebula Config') { - gauntEnv.nebula_config_path = '/tmp/'+ env.JOB_NAME + '/'+ env.BUILD_NUMBER + gauntEnv.nebula_config_path = '/tmp/'+ gauntEnv.env.JOB_NAME + '/'+ gauntEnv.env.BUILD_NUMBER if(gauntEnv.nebula_config_source == 'github'){ dir(gauntEnv.nebula_config_path){ run_i('git clone -b "' + gauntEnv.nebula_config_branch + '" ' + gauntEnv.nebula_config_repo, true) @@ -152,6 +200,16 @@ private def update_agent() { * @return Closure of stage requested */ def stage_library(String stage_name) { + stageClass = getStage(stage_name) + return stageClass.getCls() +} + +/** + * Add stage to agent pipeline + * @param stage_name String name of stage + * @return Closure of stage requested + */ +def old_stage_library(String stage_name) { switch (stage_name) { case 'UpdateBOOTFiles': println('Added Stage UpdateBOOTFiles') @@ -287,7 +345,7 @@ def stage_library(String stage_name) { if (gauntEnv.send_results){ set_elastic_field(board, 'last_failing_stage', 'UpdateBOOTFiles') set_elastic_field(board, 'last_failing_stage_failure', failing_msg) - stage_library('SendResults').call(board) + stage_library('SendResults').call(this, board) } if (is_nominal_exception) throw new NominalException('UpdateBOOTFiles failed: '+ ex.getMessage()) @@ -966,7 +1024,7 @@ private def log_artifacts(){ def command = "telemetry grab-and-log-artifacts" command += " --jenkins-server ${JENKINS_URL}" command += " --es-server ${gauntEnv.elastic_server}" - command += " --job-name ${env.JOB_NAME} --job ${env.BUILD_NUMBER}" + command += " --job-name ${gauntEnv.env.JOB_NAME} --job ${gauntEnv.env.BUILD_NUMBER}" // Pass Jenkins credentials if jenkins_credentials (credentials id) is set if (gauntEnv.credentials_id != ''){ @@ -1026,9 +1084,9 @@ private def run_agents() { println("Stage called for board: "+board) println("Num arguments for stage: "+stages[k].maximumNumberOfParameters().toString()) if ((stages[k].maximumNumberOfParameters() > 1) && gauntEnv.toolbox_generated_bootbin) - stages[k].call(board, ml_variants[ml_variant_index++]) + stages[k].call(this, board, ml_variants[ml_variant_index++]) else - stages[k].call(board) + stages[k].call(this, board) } }catch(NominalException ex){ println("oneNode: A nominal exception was encountered ${ex.getMessage()}") @@ -1052,31 +1110,31 @@ private def run_agents() { echo "Acquiring lock for ${lock_name}" lock(lock_name){ try { - docker_args_agent = docker_args + ' -v '+ gauntEnv.nebula_config_path + '/' + env.NODE_NAME + ':/tmp/nebula:ro' + docker_args_agent = docker_args + ' -v '+ gauntEnv.nebula_config_path + '/' + gauntEnv.env.NODE_NAME + ':/tmp/nebula:ro' if (enable_update_boot_pre_docker_flag) - pre_docker_closure.call(board) + pre_docker_closure.call(this, board) docker.image(docker_image_name).inside(docker_args_agent) { try { stage('Setup Docker') { - sh 'apt-get clean' - sh 'cp /tmp/nebula /etc/default/nebula' - sh 'mkdir -p ~/.pip && cp /default/pip/pip.conf ~/.pip/pip.conf || true' - sh 'cp /default/pyadi_test.yaml /etc/default/pyadi_test.yaml || true' + stepExecutor.sh 'apt-get clean' + stepExecutor.sh 'cp /tmp/nebula /etc/default/nebula' + stepExecutor.sh 'mkdir -p ~/.pip && cp /default/pip/pip.conf ~/.pip/pip.conf || true' + stepExecutor.sh 'cp /default/pyadi_test.yaml /etc/default/pyadi_test.yaml || true' def deps = check_update_container_lib(update_container) if (deps.size()>0){ setupAgent(deps, true, update_requirements) } // Above cleans up so we need to move to a valid folder - sh 'cd /tmp' + stepExecutor.sh 'cd /tmp' } if (gauntEnv.check_device_status){ stage('Check Device Status'){ def board_status = nebula("netbox.board-status --netbox-ip=" + gauntEnv.netbox_ip + " --netbox-token=" + gauntEnv.netbox_token + " --board-name=" + board) if (board_status == "Active"){ - comment = "Board is Active. Lock acquired and used by ${env.JOB_NAME} ${env.BUILD_NUMBER}" + comment = "Board is Active. Lock acquired and used by ${gauntEnv.env.JOB_NAME} ${gauntEnv.env.BUILD_NUMBER}" nebula("netbox.log-journal --netbox-ip=" + gauntEnv.netbox_ip + " --netbox-token=" + gauntEnv.netbox_token + " --board-name=" + board +" --kind='info' --comment='"+ comment + "'") }else{ - comment = "Board is not active. Skipping next stages of ${env.JOB_NAME} ${env.BUILD_NUMBER}" + comment = "Board is not active. Skipping next stages of ${gauntEnv.env.JOB_NAME} ${gauntEnv.env.BUILD_NUMBER}" nebula("netbox.log-journal --netbox-ip=" + gauntEnv.netbox_ip + " --netbox-token=" + gauntEnv.netbox_token + " --board-name=" + board +" --kind='info' --comment='" + comment + "'") throw new NominalException('Board is not active. Skipping succeeding stages.') } @@ -1092,16 +1150,16 @@ private def run_agents() { println("Stage called for board: "+board) println("Num arguments for stage: "+stages[k].maximumNumberOfParameters().toString()) if ((stages[k].maximumNumberOfParameters() > 1) && gauntEnv.toolbox_generated_bootbin) - stages[k].call(board, ml_variants[ml_variant_index++]) + stages[k].call(this, board, ml_variants[ml_variant_index++]) else - stages[k].call(board) + stages[k].call(this, board) } }catch(NominalException ex){ println("oneNodeDocker: A nominal exception was encountered ${ex.getMessage()}") println("Stopping execution of stages for ${board}") }finally { if (gauntEnv.check_device_status){ - comment = "Releasing lock by ${env.JOB_NAME} ${env.BUILD_NUMBER}" + comment = "Releasing lock by ${gauntEnv.env.JOB_NAME} ${gauntEnv.env.BUILD_NUMBER}" nebula("netbox.log-journal --netbox-ip=" + gauntEnv.netbox_ip + " --netbox-token=" + gauntEnv.netbox_token + " --board-name=" + board + " --kind='info' --comment='" + comment + "'") } println("Cleaning up after board stages"); @@ -1110,7 +1168,7 @@ private def run_agents() { } } finally { - sh 'docker ps -q -f status=exited | xargs --no-run-if-empty docker rm' + stepExecutor.sh 'docker ps -q -f status=exited | xargs --no-run-if-empty docker rm' } } } @@ -1173,6 +1231,11 @@ def get_env(String param) { return gauntEnv[param] } +def get_env() { + return gauntEnv +} + + /* * * Env setter method */ @@ -1381,12 +1444,12 @@ def isMultiBranchPipeline(repo_url) { branch = "" ref = "" println("Checking if multibranch pipeline..") - if (env.BRANCH_NAME){ + if (gauntEnv.env.BRANCH_NAME){ println("Pipeline is multibranch.") //check if the multibranch pipeline is for this repo def actualRepoUrl = scm.userRemoteConfigs[0].url if (actualRepoUrl == repo_url){ - branch = env.BRANCH_NAME + branch = gauntEnv.env.BRANCH_NAME if (branch.startsWith("PR-")) { pr_number = branch.substring(3) println "Branch is a pull request (PR number: ${pr_number})" @@ -1544,32 +1607,9 @@ private def splitMap(map, do_split=false) { return [keys, values] } -@NonCPS -private def getOnlineAgents() { - def jenkins = Jenkins.instance - def online_agents = [] - for (agent in jenkins.getNodes()) { - def computer = agent.computer - if (computer.name == 'alpine') { - continue - } - if (!computer.offline) { - if (!gauntEnv.required_agent.isEmpty()){ - if (computer.name in gauntEnv.required_agent){ - online_agents.add(computer.name) - } - }else{ - online_agents.add(computer.name) - } - } - } - println(online_agents) - return online_agents -} - private def checkOs() { - if (isUnix()) { - def uname = sh script: 'uname', returnStdout: true + if (stepExecutor.isUnix()) { + def uname = stepExecutor.sh(script: 'uname', returnStdout: true) if (uname.startsWith('Darwin')) { return 'Macos' } @@ -1594,7 +1634,7 @@ def nebula(cmd, full=false, show_log=false, report_error=false) { } cmd = 'nebula ' + cmd if (checkOs() == 'Windows') { - script_out = bat(script: cmd, returnStdout: true).trim() + script_out = stepExecutor.bat(script: cmd, returnStdout: true).trim() } else { if (report_error){ @@ -1603,12 +1643,12 @@ def nebula(cmd, full=false, show_log=false, report_error=false) { cmd = cmd + " 2>&1 | tee ${outfile}" cmd = 'set -o pipefail; ' + cmd try{ - sh cmd - if (fileExists(outfile)) - script_out = readFile(outfile).trim() + stepExecutor.sh cmd + if (stepExecutor.fileExists(outfile)) + script_out = stepExecutor.readFile(outfile).trim() }catch(Exception ex){ - if (fileExists(outfile)){ - script_out = readFile(outfile).trim() + if (stepExecutor.fileExists(outfile)){ + script_out = stepExecutor.readFile(outfile).trim() lines = script_out.split('\n') def err_line = false for (i = 1; i < lines.size(); i++) { @@ -1628,7 +1668,11 @@ def nebula(cmd, full=false, show_log=false, report_error=false) { throw new Exception("nebula failed") } }else{ - script_out = sh(script: cmd, returnStdout: true).trim() + script_out = stepExecutor.sh(script: cmd, returnStdout: true) + if (script_out == null){ + script_out = "" + } + script_out = script_out.trim() } } // Remove lines @@ -1666,21 +1710,21 @@ def sendLogsToElastic(... args) { cmd = 'telemetry log-boot-logs ' + cmd println(cmd) if (checkOs() == 'Windows') { - script_out = bat(script: cmd, returnStdout: true).trim() + script_out = stepExecutor.bat(script: cmd, returnStdout: true)?.trim() } else { - script_out = sh(script: cmd, returnStdout: true).trim() + script_out = stepExecutor.sh(script: cmd, returnStdout: true)?.trim() } // Remove lines out = '' if (!full) { - lines = script_out.split('\n') - if (lines.size() == 1) { + lines = script_out?.split('\n') + if (lines?.size() == 1) { return script_out } out = '' added = 0 - for (i = 1; i < lines.size(); i++) { + for (i = 1; i < lines?.size(); i++) { if (lines[i].contains('WARNING')) { continue } @@ -1703,7 +1747,7 @@ def String getURIFromSerial(String board){ serial_no = nebula('update-config board-config instr-serial --board-name='+board) } cmd="iio_info -s | grep serial="+serial_no+" | grep -Po \"\\[.*:.*\" | sed 's/.\$//' | cut -c 2-" - instr_uri = sh(script:cmd, returnStdout: true).trim() + instr_uri = stepExecutor.sh(script:cmd, returnStdout: true).trim() return instr_uri } @@ -1725,8 +1769,8 @@ private def install_nebula(update_requirements=false) { extensions: [[$class: 'LocalBranch', localBranch: "**"]], userRemoteConfigs: [[credentialsId: '', url: "${gauntEnv.nebula_repo}"]] ]) - sh 'pip3 uninstall nebula -y || true' - sh 'pip3 install .' + stepExecutor.sh 'pip3 uninstall nebula -y || true' + stepExecutor.sh 'pip3 install .' } } @@ -1735,11 +1779,11 @@ private def install_libiio() { run_i('git clone -b ' + gauntEnv.libiio_branch + ' ' + gauntEnv.libiio_repo, true) dir('libiio') { - bat 'mkdir build' - bat('build') + stepExecutor.bat 'mkdir build' + dir('build') { - bat 'cmake .. -DPYTHON_BINDINGS=ON -DWITH_SERIAL_BACKEND=ON -DHAVE_DNS_SD=OFF' - bat 'cmake --build . --config Release --install' + stepExecutor.bat 'cmake .. -DPYTHON_BINDINGS=ON -DWITH_SERIAL_BACKEND=ON -DHAVE_DNS_SD=OFF' + stepExecutor.bat 'cmake --build . --config Release --install' } } } @@ -1751,16 +1795,16 @@ private def install_libiio() { extensions: [[$class: 'LocalBranch', localBranch: "**"]], userRemoteConfigs: [[credentialsId: '', url: "${gauntEnv.libiio_repo}"]] ]) - sh 'mkdir -p build' + stepExecutor.sh 'mkdir -p build' dir('build') { - sh 'cmake .. -DPYTHON_BINDINGS=ON -DWITH_SERIAL_BACKEND=ON -DHAVE_DNS_SD=OFF' - sh 'make' - sh 'sudo make install' - sh 'ldconfig' + stepExecutor.sh 'cmake .. -DPYTHON_BINDINGS=ON -DWITH_SERIAL_BACKEND=ON -DHAVE_DNS_SD=OFF' + stepExecutor.sh 'make' + stepExecutor.sh 'sudo make install' + stepExecutor.sh 'ldconfig' // install python bindings dir('bindings/python'){ - sh 'python3 setup.py install' + stepExecutor.sh 'python3 setup.py install' } } } @@ -1777,7 +1821,7 @@ private def install_telemetry(update_requirements=false){ run_i('python setup.py install', true) } }else{ - // sh 'pip3 uninstall telemetry -y || true' + // stepExecutor.sh 'pip3 uninstall telemetry -y || true' def scmVars = checkout([ $class : 'GitSCM', branches : [[name: "*/${gauntEnv.telemetry_branch}"]], @@ -1787,26 +1831,7 @@ private def install_telemetry(update_requirements=false){ if (update_requirements){ run_i('pip3 install -r requirements.txt', true) } - sh 'pip3 install .' - } -} - -private def setup_locale() { - sh 'sudo apt-get install -y locales' - sh 'export LC_ALL=en_US.UTF-8 && export LANG=en_US.UTF-8 && export LANGUAGE=en_US.UTF-8 && locale-gen en_US.UTF-8' -} - -private def setup_libserialport() { - sh 'sudo apt-get install -y autoconf automake libtool' - sh 'git clone https://github.com/sigrokproject/libserialport.git' - dir('libserialport'){ - sh './autogen.sh' - sh './configure --prefix=/usr/sp' - sh 'make' - sh 'make install' - sh 'cp -r /usr/sp/lib/* /usr/lib/x86_64-linux-gnu/' - sh 'cp /usr/sp/include/* /usr/include/' - sh 'date -r /usr/lib/x86_64-linux-gnu/libserialport.so.0' + stepExecutor.sh 'pip3 install .' } } @@ -1866,7 +1891,7 @@ def get_gitsha(String board){ return } - if (fileExists('outs/properties.yaml')){ + if (stepExecutor.fileExists('outs/properties.yaml')){ dir ('outs'){ script{ properties = readYaml file: 'properties.yaml' } } @@ -1877,9 +1902,9 @@ def get_gitsha(String board){ hdl_hash = properties.hdl_git_sha + " (" + properties.bootpartition_folder + ")" linux_hash = properties.linux_git_sha + " (" + properties.bootpartition_folder + ")" } - } else if(fileExists('outs/properties.txt')){ + } else if(stepExecutor.fileExists('outs/properties.txt')){ dir ('outs'){ - def file = readFile 'properties.txt' + def file = stepExecutor.readFile 'properties.txt' lines = file.readLines() for (line in lines){ echo line @@ -1953,17 +1978,17 @@ private def extractLockName(String bname, String agent){ return lockName } -private def run_i(cmd, do_retry=false) { +def run_i(cmd, do_retry=false) { def retry_count = 1 if(do_retry){ retry_count = gauntEnv.max_retry } - retry(retry_count){ + stepExecutor.retry(retry_count){ if (checkOs() == 'Windows') { - bat cmd + stepExecutor.bat cmd } else { - sh cmd + stepExecutor.sh cmd } } } @@ -1979,9 +2004,9 @@ private def String getStackTrace(Throwable aThrowable){ private def createMFile(){ // Utility method to write matlab commands in a .m file def String command_oneline = gauntEnv.matlab_commands.join(";") - writeFile file: 'matlab_commands.m', text: command_oneline - sh 'ls -l matlab_commands.m' - sh 'cat matlab_commands.m' + stepExecutor.writeFile file: 'matlab_commands.m', text: command_oneline + stepExecutor.sh 'ls -l matlab_commands.m' + stepExecutor.sh 'cat matlab_commands.m' } private def parseForLogging (String stage, String xmlFile, String board) { @@ -1992,6 +2017,6 @@ private def parseForLogging (String stage, String xmlFile, String board) { forLogging."${stage_logs}".each { cmd = 'cat ' + xmlFile + ' | sed -rn \'s/.*' cmd+= it + '="([0-9]+)".*/\\1/p\'' - set_elastic_field(board.replaceAll('_', '-'), stage + '_' + it, sh(returnStdout: true, script: cmd).trim()) + set_elastic_field(board.replaceAll('_', '-'), stage + '_' + it, stepExecutor.sh(returnStdout: true, script: cmd).trim()) } } diff --git a/src/sdg/IStepExecutor.groovy b/src/sdg/IStepExecutor.groovy new file mode 100644 index 00000000..f87b6fed --- /dev/null +++ b/src/sdg/IStepExecutor.groovy @@ -0,0 +1,38 @@ +package sdg + +import jenkins.model.Jenkins + +interface IStepExecutor { + + // Jenkins steps as needed + Integer sh(String command) + String sh(Map kwargs) + Integer bat(String command) + String bat(Map kwargs) + void error(String message) + void stage(String name, Closure cls) + void echo(String message) + void println(String message) + Map getGauntEnv( + String hdlBranch, + String linuxBranch, + String bootPartitionBranch, + String firmwareVersion, + String bootfile_source + ) + void retry(int count, Closure cls) + void archiveArtifacts(Map kwargs) + void junit(Map kwargs) + void publishHTML(Map kwargs) + void checkout(Map kwargs) + boolean isUnix() + boolean fileExists(String file) + String readFile(String file) + void writeFile(Map kwargs) + void dir(String dir, Closure cls) + void unstable(String message) + void sleep(int seconds) + void xunit(List testResults) + Object CTest(Map kwargs) + Map getEnv() +} \ No newline at end of file diff --git a/src/sdg/Library.groovy b/src/sdg/Library.groovy deleted file mode 100644 index 42ac615e..00000000 --- a/src/sdg/Library.groovy +++ /dev/null @@ -1,47 +0,0 @@ - -class Library { - - def stage(String stage_name) { - switch (stage_name) { - case 'UpdateBOOTFiles': - println('Added Stage UpdateBOOTFiles') - cls = { - stage('Update BOOT Files') { - nebula('dl.bootfiles --design-name=' + board) - nebula('manager.update-boot-files --folder=outs') - } - }; - break - case 'CollectLogs': - println('Added Stage CollectLogs') - cls = { - stage('Collect Logs') { - echo 'Collect Logs' - } - }; - break - case 'PyADITests': - cls = { - stage('Run Python Tests') { - ip = nebula('uart.get-ip') - println('IP: ' + ip) - sh 'git clone https://github.com/analogdevicesinc/pyadi-iio.git' - dir('pyadi-iio') - { - sh 'ls' - run('pip3 install -r requirements.txt') - run('pip3 install -r requirements_dev.txt') - run('pip3 install pylibiio') - run("python3 -m pytest -v -s --uri='ip:"+ip+"' -m " + board.replaceAll('-', '_')) - } - } - } - break - default: - throw new Exception('Unknown library stage: ' + stage_name) - } - - return cls - } - -} diff --git a/src/sdg/Logger.groovy b/src/sdg/Logger.groovy new file mode 100644 index 00000000..8e00ea44 --- /dev/null +++ b/src/sdg/Logger.groovy @@ -0,0 +1,30 @@ +package sdg + +import sdg.Gauntlet + +class Logger { + + private def gauntlet + + def Logger(Gauntlet gauntlet){ + this.gauntlet = gauntlet + } + + public void info(String message) { + if(gauntlet.get_env("debug_level") >= 3){ + gauntlet.stepExecutor.echo "[INFO] ${message}" + } + } + + public void warning(String message) { + if(gauntlet.get_env("debug_level") >= 2){ + gauntlet.stepExecutor.echo "[WARNING] ${message}" + } + } + + public void error(String message) { + if(gauntlet.get_env("debug_level") >= 1){ + gauntlet.stepExecutor.echo "[ERROR] ${message}" + } + } +} \ No newline at end of file diff --git a/src/sdg/StepExecutor.groovy b/src/sdg/StepExecutor.groovy new file mode 100644 index 00000000..e2985dbe --- /dev/null +++ b/src/sdg/StepExecutor.groovy @@ -0,0 +1,150 @@ +package sdg + +import sdg.IStepExecutor +import jenkins.model.Jenkins + +class StepExecutor implements IStepExecutor{ + // this will be provided by the vars script and + // let's us access Jenkins steps + private _steps + + StepExecutor(steps) { + this._steps = steps + } + + @Override + Integer sh(String command){ + return this._steps.sh(command) + } + + @Override + String sh(Map kwargs = [:]){ + return this._steps.sh(kwargs) + } + + @Override + Integer bat(String command){ + return this._steps.bat(command) + } + + @Override + String bat(Map kwargs = [:]){ + return this._steps.bat(kwargs) + } + + @Override + void error(String message) { + this._steps.error(message) + } + + @Override + void stage(String name, Closure cls) { + this._steps.stage(name,cls) + } + + @Override + void echo(String message){ + this._steps.echo(message) + } + + @Override + void println(String message){ + this._steps.println(message) + } + + @Override + Map getGauntEnv( + String hdlBranch, + String linuxBranch, + String bootPartitionBranch, + String firmwareVersion, + String bootfile_source + ){ + this._steps.getGauntEnv( + hdlBranch, + linuxBranch, + bootPartitionBranch, + firmwareVersion, + bootfile_source, + ) + } + + @Override + void retry(int count, Closure cls){ + this._steps.retry(count, cls) + } + + @Override + void archiveArtifacts(Map kwargs = [:]) { + this._steps.archiveArtifacts(kwargs) + } + + @Override + void junit(Map kwargs = [:]) { + this._steps.junit(kwargs) + } + + @Override + void publishHTML(Map kwargs = [:]) { + this._steps.publishHTML(kwargs) + } + + @Override + void checkout(Map kwargs = [:]) { + this._steps.checkout(kwargs) + } + + @Override + boolean isUnix() { + this._steps.isUnix() + } + + @Override + boolean fileExists(String file) { + return this._steps.fileExists(file) + } + + @Override + String readFile(String file) { + return this._steps.readFile(file) + } + + @Override + void writeFile(Map kwargs = [:]) { + this._steps.writeFile(kwargs) + } + + @Override + void dir(String dir, Closure cls) { + this._steps.dir(dir, cls) + } + + @Override + void unstable(String message) { + this._steps.unstable(message) + } + + @Override + void sleep(int seconds){ + this._steps.sleep(seconds) + } + + @Override + void xunit(List testResults) { + this._steps.xunit(testResults) + } + + private Map _mockEnv = null + + Map getEnv() { + return _mockEnv ?: (_steps?.env ?: [:]) + } + + Object CTest(Map kwargs) { + return _steps.CTest(kwargs) + } + + void setMockEnv(Map env) { + this._mockEnv = env + } +} diff --git a/src/sdg/ioc/ContextRegistry.groovy b/src/sdg/ioc/ContextRegistry.groovy new file mode 100644 index 00000000..ee96cfd4 --- /dev/null +++ b/src/sdg/ioc/ContextRegistry.groovy @@ -0,0 +1,17 @@ +package sdg.ioc + +class ContextRegistry implements Serializable { + private static IContext _context + + static void registerContext(IContext context) { + _context = context + } + + static void registerDefaultContext(Object steps) { + _context = new DefaultContext(steps) + } + + static IContext getContext() { + return _context + } +} diff --git a/src/sdg/ioc/DefaultContext.groovy b/src/sdg/ioc/DefaultContext.groovy new file mode 100644 index 00000000..83a6a1b8 --- /dev/null +++ b/src/sdg/ioc/DefaultContext.groovy @@ -0,0 +1,24 @@ +package sdg.ioc + +import sdg.IStepExecutor +import sdg.StepExecutor + +class DefaultContext implements IContext, Serializable { + // the same as in the StepExecutor class + private _steps + + DefaultContext(steps) { + this._steps = steps + } + + @Override + IStepExecutor getStepExecutor() { + return new StepExecutor(this._steps) + } + + @Override + Boolean isDefault(){ + return true + } + +} diff --git a/src/sdg/ioc/IContext.groovy b/src/sdg/ioc/IContext.groovy new file mode 100644 index 00000000..c41e2170 --- /dev/null +++ b/src/sdg/ioc/IContext.groovy @@ -0,0 +1,8 @@ +package sdg.ioc + +import sdg.IStepExecutor + +interface IContext { + IStepExecutor getStepExecutor() + Boolean isDefault() +} diff --git a/src/sdg/stages/CaptureIIOContext.groovy b/src/sdg/stages/CaptureIIOContext.groovy new file mode 100644 index 00000000..4b5ee300 --- /dev/null +++ b/src/sdg/stages/CaptureIIOContext.groovy @@ -0,0 +1,72 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage + +/** + * The CaptureIIOContext class implements the IStage interface and provides + * functionality to capture the IIO context of a board and log messages at different levels. + */ +class CaptureIIOContext implements IStage { + /** + * Returns the name of the stage. + * + * @return The name of the stage, which is "CaptureIIOContext". + */ + String getStageName(){ + return "CaptureIIOContext" + } + + /** + * Executes the stage steps, capturing the IIO context of the board and logging messages. + * @return A closure that takes a Gauntlet instance and a board name as parameters. + * The closure executes the stage steps within a stage block. + * + * @param gauntlet The Gauntlet instance used to set environment variables and log messages. + * @param board The board name for which the stage is being executed. + */ + Closure getCls(){ + return { gauntlet, board -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } + + void stageSteps(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running ${getStageName()} for ${board}") + logger.info("Installing iio-emu") + steps.sh 'git clone https://github.com/analogdevicesinc/libtinyiiod.git' + steps.dir('libtinyiiod') + { + steps.sh 'mkdir -p build' + steps.dir('build') + { + steps.sh 'cmake -DBUILD_EXAMPLES=OFF ..' + steps.sh 'make' + steps.sh 'make install' + steps.sh 'ldconfig' + } + } + steps.sh 'git clone -b v0.1.0 https://github.com/analogdevicesinc/iio-emu.git' + steps.dir('iio-emu') + { + steps.sh 'mkdir -p build' + steps.dir('build') + { + steps.sh 'cmake -DBUILD_TOOLS=ON ..' + steps.sh 'make' + steps.sh 'make install' + steps.sh 'ldconfig' + } + } + + logger.info("Capturing IIO context with iio-emu") + def ip = gauntlet.nebula('update-config network-config dutip --board-name='+board) + steps.sh 'xml_gen ip:'+ip+' > "'+board+'.xml"' + steps.archiveArtifacts artifacts: '*.xml' + } +} \ No newline at end of file diff --git a/src/sdg/stages/IStage.groovy b/src/sdg/stages/IStage.groovy new file mode 100644 index 00000000..0b3df09e --- /dev/null +++ b/src/sdg/stages/IStage.groovy @@ -0,0 +1,9 @@ +package sdg.stages + +import sdg.Gauntlet + +interface IStage { + + String getStageName() + Closure getCls() +} \ No newline at end of file diff --git a/src/sdg/stages/KuiperCheck.groovy b/src/sdg/stages/KuiperCheck.groovy new file mode 100644 index 00000000..724f4f73 --- /dev/null +++ b/src/sdg/stages/KuiperCheck.groovy @@ -0,0 +1,84 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage +import sdg.NominalException + +/** + * The KuiperCheck class implements the IStage interface + * + */ +class KuiperCheck implements IStage { + /** + * Returns the name of the stage. + * + * @return The name of the stage, which is "KuiperCheck". + */ + String getStageName(){ + return "KuiperCheck" + } + + /** + + */ + Closure getCls(){ + return { gauntlet, board -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } + /** + * Executes the steps for the LibAD9361Tests stage. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board to be power cycled. + */ + void stageSteps(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running ${getStageName()} for ${board}") + try{ + // Download tool + gauntlet.run_i( + "git clone -b $gauntEnv.kuiper_checker_branch $gauntEnv.kuiper_checker_repo" + ) + steps.dir('kuiper-post-build-checker'){ + // install kpbc requirements, retry on failure + gauntlet.run_i('pip3 install -r requirements.txt', true) + // fetch kuiper gen, retry on failure + gauntlet.run_i('invoke fetchkuipergen', true) + // get board ip + def ip = gauntlet.nebula('update-config network-config dutip --board-name='+board) + // execute test + def cmd = "python3 -m pytest -v --html=testhtml/$board" + "_kpbc_report.html" + cmd = cmd + " --junitxml=testxml/$board" + "_kpbc_reports.xml" + cmd = cmd + " --ip=$ip -m \"not hardware_check\" --capture=tee-sys" + def statusCode = steps.sh(script:cmd, returnStatus:true) + // generate html report + if (steps.fileExists("testhtml/$board" + "_kpbc_report.html")){ + steps.publishHTML(target : [ + escapeUnderscores: false, + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'testhtml', + reportFiles: "$board" + "_kpbc_report.html", + reportName: board, + reportTitles: board]) + } + // TODO: parse result for elastic logging + // throw exception if pytest failed + if ((statusCode != 5) && (statusCode != 0)){ + // Ignore error 5 which means no tests were run + throw new NominalException('Kuiper Check Failed') + } + } + } + finally{ + // archive result + steps.junit testResults: 'kuiper-post-build-checker/testxml/*.xml', allowEmptyResults: true + } + } +} \ No newline at end of file diff --git a/src/sdg/stages/LibAD9361Tests.groovy b/src/sdg/stages/LibAD9361Tests.groovy new file mode 100644 index 00000000..9e9c2baf --- /dev/null +++ b/src/sdg/stages/LibAD9361Tests.groovy @@ -0,0 +1,78 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage + +/** + * The LibAD9361Tests class implements the IStage interface + * and provides functionality to run libad9361 tests on the target board. + */ +class LibAD9361Tests implements IStage { + /** + * Returns the name of the stage. + * + * @return The name of the stage, which is "LibAD9361Tests". + */ + String getStageName(){ + return "LibAD9361Tests" + } + + /** + + */ + Closure getCls(){ + return { gauntlet, board -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } + /** + * Executes the steps for the LibAD9361Tests stage. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board to be power cycled. + */ + void stageSteps(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running ${getStageName()} for ${board}") + def supported = false + def supported_boards = ["adrv9361", "adrv9364", "ad9361", "ad9364", "pluto"] + for(s in supported_boards){ + if (board.contains(s)){ + supported = true + } + } + if(supported && gauntEnv.libad9361_iio_branch != null){ + try{ + def ip = gauntlet.nebula("update-config -s network-config -f dutip --board-name="+board) + gauntlet.run_i('sudo rm -rf libad9361-iio') + gauntlet.run_i('git clone -b '+ gauntEnv.libad9361_iio_branch + ' ' + gauntEnv.libad9361_iio_repo, true) + steps.dir('libad9361-iio') + { + steps.sh('mkdir -p build') + steps.dir('build') + { + steps.sh('cmake -DPYTHON_BINDINGS=ON ..') + steps.sh('make') + steps.sh('make install') + steps.sh('ldconfig') + steps.sh('URI_AD9361="ip:'+ip+'" ctest -T test --no-compress-output -V') + } + } + }catch(Exception ex){ + steps.unstable("LibAD9361Tests Failed: ${ex.getMessage()}") + }finally{ + steps.dir('libad9361-iio/build'){ + steps.sh("mv Testing ${board}") + steps.xunit([steps.CTest(deleteOutputFiles: true, failIfNotNew: true, pattern: "${board}/**/*.xml", skipNoTestFiles: false, stopProcessingIfError: true)]) + steps.archiveArtifacts artifacts: "${board}/**/*.xml", followSymlinks: false, allowEmptyArchive: true + } + } + }else{ + logger.info("LibAD9361Tests: Skipping board: "+board) + } + } +} \ No newline at end of file diff --git a/src/sdg/stages/LinuxTests.groovy b/src/sdg/stages/LinuxTests.groovy new file mode 100644 index 00000000..20da123f --- /dev/null +++ b/src/sdg/stages/LinuxTests.groovy @@ -0,0 +1,112 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage +import sdg.NominalException + +/** + * This class represents the "LinuxTests" stage in the Jenkins pipeline. + * It contains methods related to running Linux tests on the board. + */ +class LinuxTests implements IStage { + + /** + * Retrieves the name of the stage. + * + * @return A string representing the name of the stage, which is "LinuxTests". + */ + String getStageName(){ + return "LinuxTests" + } + + /** + * Returns a closure that executes a stage in the Jenkins pipeline. + * + * @return Closure that takes two parameters: gauntlet and board + * The closure executes a stage using the gauntlet's stepExecutor and calls the stageSteps method. + * + * @param gauntlet The gauntlet object that contains the stepExecutor. + * @param board The board parameter to be passed to the stageSteps method. + */ + Closure getCls(){ + return { gauntlet, board -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } + + /** + * Executes the steps for the LinuxTests stage. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board on which the Linux tests are being run. + */ + void stageSteps(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running ${getStageName()} for ${board}") + def failed_test = '' + def devs = [] + def missing_devs = [] + try { + // run_i('pip3 install pylibiio',true) + //def ip = nebula('uart.get-ip') + def ip = gauntlet.nebula('update-config network-config dutip --board-name='+board) + + try{ + gauntlet.nebula('driver.check-iio-devices --uri="ip:'+ip+'" --board-name='+board, true, true, true) + }catch(Exception ex) { + failed_test = failed_test + "[iio_devices check failed: ${ex.getMessage()}]" + missing_devs = Eval.me(ex.getMessage().split('\n').last().split('not found')[1].replaceAll("'\$","")) + steps.writeFile(file: board+'_missing_devs.log', text: missing_devs.join("\n")) + gauntlet.set_elastic_field(board, 'drivers_missing', missing_devs.size().toString()) + } + // get drivers enumerated + devs = Eval.me(gauntlet.nebula('update-config driver-config iio_device_names -b '+board, false, true, false)) + devs = devs.minus(missing_devs) + steps.writeFile(file: board+'_enumerated_devs.log', text: devs.join("\n")) + gauntlet.set_elastic_field(board, 'drivers_enumerated', devs.size().toString()) + + try{ + steps.sh('iio_info --uri=ip:'+ip) + gauntlet.nebula("net.check-dmesg --ip='"+ip+"' --board-name="+board) + }catch(Exception ex) { + failed_test = failed_test + "[dmesg check failed: ${ex.getMessage()}]" + } + + if (gauntEnv.test_adi_diagnostics) { + try{ + if (!gauntEnv.firmware_boards.contains(board)){ + try{ + gauntlet.nebula('update-config board-config serial --board-name='+board) + gauntlet.nebula("net.run-diagnostics --ip='"+ip+"' --board-type=rpi --board-name="+board, true, true, true) + }catch(Exception ex){ + gauntlet.nebula("net.run-diagnostics --ip='"+ip+"' --board-name="+board, true, true, true) + } + steps.archiveArtifacts artifacts: '*_diag_report.tar.bz2', followSymlinks: false, allowEmptyArchive: true + } + }catch(Exception ex) { + failed_test = failed_test + " [diagnostics failed: ${ex.getMessage()}]" + } + } + + if(failed_test && !failed_test.allWhitespace){ + steps.unstable("Linux Tests Failed: ${failed_test}") + } + }catch(Exception ex) { + throw new NominalException(ex.getMessage()) + }finally{ + // count dmesg errs and warns + gauntlet.set_elastic_field(board, 'dmesg_errs', steps.sh(returnStdout: true, script: 'cat dmesg_err_filtered.log | wc -l').trim()) + gauntlet.set_elastic_field(board, 'dmesg_warns', steps.sh(returnStdout: true, script: 'cat dmesg_warn.log | wc -l').trim()) + // Rename logs + gauntlet.run_i("if [ -f dmesg.log ]; then mv dmesg.log dmesg_" + board + ".log; fi") + gauntlet.run_i("if [ -f dmesg_err_filtered.log ]; then mv dmesg_err_filtered.log dmesg_" + board + "_err.log; fi") + gauntlet.run_i("if [ -f dmesg_warn.log ]; then mv dmesg_warn.log dmesg_" + board + "_warn.log; fi") + steps.archiveArtifacts artifacts: '*.log', followSymlinks: false, allowEmptyArchive: true + } + + } +} diff --git a/src/sdg/stages/MATLABTests.groovy b/src/sdg/stages/MATLABTests.groovy new file mode 100644 index 00000000..a5384b81 --- /dev/null +++ b/src/sdg/stages/MATLABTests.groovy @@ -0,0 +1,124 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage +import sdg.NominalException + +/** + * The MATLABTests class implements the IStage interface + * and provides functionality to run matlab tests on the target board. + */ +class MATLABTests implements IStage { + /** + * Returns the name of the stage. + * + * @return The name of the stage, which is "MATLABTests". + */ + String getStageName(){ + return "MATLABTests" + } + + /** + + */ + Closure getCls(){ + return { gauntlet, board -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } + /** + * Executes the steps for the LibAD9361Tests stage. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board to be power cycled. + */ + void stageSteps(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running ${getStageName()} for ${board}") + def ip = gauntlet.nebula('update-config network-config dutip --board-name='+board) + def description = "" + def xmlFile = board+'_HWTestResults.xml' + steps.sh('cp -r /root/.matlabro /root/.matlab') + def result = gauntlet.isMultiBranchPipeline(gauntEnv.matlab_repo) + result.branch = !result.isMultiBranch ? gauntEnv.matlab_branch : result.branch + def cloneConfigs = [credentialsId: '', url: gauntEnv.matlab_repo] + cloneConfigs['refspec'] = result.isMultiBranch ? result.ref : cloneConfigs['refspec'] + steps.checkout([ + $class : 'GitSCM', + branches : [[name: result.branch]], + userRemoteConfigs: [cloneConfigs], + extensions: [ + [$class: 'SubmoduleOption', recursiveSubmodules: true, trackingSubmodules: false] + ] + ]) + gauntlet.createMFile() + + // Declare statusCode at method scope to ensure it's accessible in try, catch, and finally blocks + def statusCode = 0 + + try{ + def cmd = 'chown -R user $(pwd) ; ' + cmd += 'sudo -u user IIO_URI="ip:'+ip+'" board="'+board+'" M2K_URI="'+gauntlet.getURIFromSerial(board)+'"' + cmd += ' elasticserver='+gauntEnv.elastic_server+' timeout -s KILL '+gauntEnv.matlab_timeout + cmd += ' /usr/local/MATLAB/'+gauntEnv.matlab_release+'/bin/matlab -nosplash -nodesktop -nodisplay' + cmd += ' -r "run(\'matlab_commands.m\');exit"' + + // Check if MATLAB executable exists before trying to run it + def matlabPath = '/usr/local/MATLAB/'+gauntEnv.matlab_release+'/bin/matlab' + if (!steps.fileExists(matlabPath)) { + throw new NominalException("MATLAB executable not found at: " + matlabPath) + } + + statusCode = steps.sh(script:cmd, returnStatus:true) + }catch (Exception ex){ + xmlFile = steps.sh(returnStdout: true, script: 'ls | grep _*Results.xml').trim() + // If we reach here due to sh command failure, assume error status + if (statusCode == 0) { + statusCode = 1 // Assume error encountered + } + throw new NominalException(ex.getMessage()) + }finally{ + steps.junit testResults: '*.xml', allowEmptyResults: true + // archiveArtifacts artifacts: xmlFile, followSymlinks: false, allowEmptyArchive: true + // get MATLAB hardware test results for logging + if(steps.fileExists(xmlFile)){ + try{ + gauntlet.parseForLogging ('matlab', xmlFile, board) + }catch(Exception ex){ + logger.info('Parsing MATLAB hardware results failed') + logger.info(gauntlet.getStackTrace(ex)) + } + } + // Print test result summary and set stage status depending on test result + if (statusCode != 0) { + // Note: currentBuild access would need to be handled through Jenkins context + logger.info("MATLAB tests failed with status code: " + statusCode) + } + handleTestResult(steps, statusCode) + } + } + + /** + * Handles the test result based on status code + * @param steps The step executor + * @param statusCode The status code from MATLAB test execution + */ + void handleTestResult(steps, statusCode) { + def intStatusCode = statusCode as Integer + switch (intStatusCode) { + case 1: + steps.unstable("MATLAB: Error encountered when running the tests.") + break + case 2: + steps.unstable("MATLAB: Some tests failed.") + break + case 3: + steps.unstable("MATLAB: Some tests did not run to completion.") + break + } + } +} \ No newline at end of file diff --git a/src/sdg/stages/PowerCycleBoard.groovy b/src/sdg/stages/PowerCycleBoard.groovy new file mode 100644 index 00000000..277dec2c --- /dev/null +++ b/src/sdg/stages/PowerCycleBoard.groovy @@ -0,0 +1,51 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage + +/** + * The PowerCycleBoard class implements the IStage interface and provides + * functionality to power cycle a board and log messages at different levels. + */ +class PowerCycleBoard implements IStage { + /** + * Returns the name of the stage. + * + * @return The name of the stage, which is "PowerCycleBoard". + */ + String getStageName(){ + return "PowerCycleBoard" + } + + /** + * Executes the stage steps, power cycling the board and logging messages. + * @return A closure that takes a Gauntlet instance and a board name as parameters. + * The closure executes the stage steps within a stage block. + * + * @param gauntlet The Gauntlet instance used to set environment variables and log messages. + * @param board The board name for which the stage is being executed. + */ + Closure getCls(){ + return { gauntlet, board -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } + /** + * Executes the steps for the PowerCycleBoard stage. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board to be power cycled. + */ + void stageSteps(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running ${getStageName()} for ${board}") + def pdutype = gauntlet.nebula('update-config pdu-config pdu_type --board-name='+board) + def outlet = gauntlet.nebula('update-config pdu-config outlet --board-name='+board) + gauntlet.nebula('pdu.power-cycle -b ' + board + ' -p ' + pdutype + ' -o ' + outlet) + logger.info("Power cycle done for ${board}") + } +} \ No newline at end of file diff --git a/src/sdg/stages/PyADITests.groovy b/src/sdg/stages/PyADITests.groovy new file mode 100644 index 00000000..9bde8a93 --- /dev/null +++ b/src/sdg/stages/PyADITests.groovy @@ -0,0 +1,147 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage + +/** + * The PyADITests class implements the IStage interface + * and provides functionality to run pyadi-iio tests on the target board. + */ +class PyADITests implements IStage { + /** + * Returns the name of the stage. + * + * @return The name of the stage, which is "PyADITests". + */ + String getStageName(){ + return "PyADITests" + } + + /** + + */ + Closure getCls(){ + return { gauntlet, board -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } + /** + * Executes the steps for the PyADITests stage. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board to be power cycled. + */ + void stageSteps(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running ${getStageName()} for ${board}") + try + { + //def ip = nebula('uart.get-ip') + def ip; + def serial; + def baudrate; + def uri; + def description = "" + def pytest_attachment = null + logger.info('IP: ' + ip) + // temporarily get pytest-libiio from another source + gauntlet.run_i('git clone -b "' + gauntEnv.pytest_libiio_branch + '" ' + gauntEnv.pytest_libiio_repo, true) + steps.dir('pytest-libiio'){ + gauntlet.run_i('pip3 install .', true) + } + //install libad9361 python bindings + try{ + steps.sh 'python3 -c "import ad9361"' + }catch (Exception ex){ + gauntlet.run_i('sudo rm -rf libad9361-iio') + gauntlet.run_i('git clone -b '+ gauntEnv.libad9361_iio_branch + ' ' + gauntEnv.libad9361_iio_repo, true) + steps.dir('libad9361-iio'){ + steps.sh('mkdir -p build') + steps.dir('build'){ + steps.sh('sudo cmake -DPYTHON_BINDINGS=ON ..') + steps.sh('sudo make') + steps.sh('sudo make install') + steps.sh('ldconfig') + } + } + } + //scm pyadi-iio + steps.dir('pyadi-iio'){ + def result = gauntlet.isMultiBranchPipeline(gauntEnv.pyadi_iio_repo) + result.branch = !result.isMultiBranch ? gauntEnv.pyadi_iio_branch : result.branch + def cloneConfigs = [credentialsId: '', url: gauntEnv.pyadi_iio_repo] + cloneConfigs['refspec'] = result.isMultiBranch ? result.ref : cloneConfigs['refspec'] + steps.checkout([ + $class : 'GitSCM', + branches : [[name: result.branch]], + userRemoteConfigs: [cloneConfigs] + ]) + } + + steps.dir('pyadi-iio') + { + gauntlet.run_i('pip3 install -r requirements.txt', true) + gauntlet.run_i('pip3 install -r requirements_dev.txt', true) + gauntlet.run_i('pip3 install pylibiio', true) + gauntlet.run_i('mkdir testxml') + gauntlet.run_i('mkdir testhtml') + if (gauntEnv.iio_uri_source == "ip"){ + ip = gauntlet.nebula('update-config network-config dutip --board-name='+board) + uri = "ip:" + ip; + }else{ + serial = gauntlet.nebula('update-config uart-config address --board-name='+board) + baudrate = gauntlet.nebula('update-config uart-config baudrate --board-name='+board) + uri = "serial:" + serial + "," + baudrate + } + def check = gauntlet.check_for_marker(board) + board = board.replaceAll('-', '_') + def board_name = check.board_name.replaceAll('-', '_') + def marker = check.marker + def cmd = "python3 -m pytest --html=testhtml/report.html --junitxml=testxml/" + board + "_reports.xml" + cmd += " --adi-hw-map -v -k 'not stress and not prod' -s --uri="+uri+" -m " + board_name + cmd += " --scan-verbose --capture=tee-sys" + marker + def statusCode = steps.sh(script:cmd, returnStatus:true) + + // generate html report + if (steps.fileExists('testhtml/report.html')){ + steps.publishHTML(target : [ + escapeUnderscores: false, + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'testhtml', + reportFiles: 'report.html', + reportName: board, + reportTitles: board]) + } + + // get pytest results for logging + def xmlFile = 'testxml/' + board + '_reports.xml' + if(steps.fileExists(xmlFile)){ + try{ + gauntlet.parseForLogging ('pytest', xmlFile, board) + }catch(Exception ex){ + logger.info('Parsing pytest results failed') + logger.info(gauntlet.getStackTrace(ex)) + } + pytest_attachment = board+"_reports.xml" + } + + // throw exception if pytest failed + if ((statusCode != 5) && (statusCode != 0)){ + // Ignore error 5 which means no tests were run + steps.unstable("PyADITests Failed") + } + } + } + finally + { + steps.archiveArtifacts artifacts: 'pyadi-iio/testxml/*.xml', followSymlinks: false, allowEmptyArchive: true + steps.junit testResults: 'pyadi-iio/testxml/*.xml', allowEmptyResults: true + } + } +} \ No newline at end of file diff --git a/src/sdg/stages/RecoverBoard.groovy b/src/sdg/stages/RecoverBoard.groovy new file mode 100644 index 00000000..516cb5cb --- /dev/null +++ b/src/sdg/stages/RecoverBoard.groovy @@ -0,0 +1,110 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage +import sdg.NominalException + +/** + * This class represents the "RecoverBoard" stage in the Jenkins pipeline. + * It contains methods related to recovering the board. + */ +class RecoverBoard implements IStage { + + /** + * Retrieves the name of the stage. + * + * @return A string representing the name of the stage, which is "RecoverBoard". + */ + String getStageName(){ + return "RecoverBoard" + } + + /** + * Returns a closure that executes a stage in the Jenkins pipeline. + * + * @return Closure that takes two parameters: gauntlet and board + * The closure executes a stage using the gauntlet's stepExecutor and calls the stageSteps method. + * + * @param gauntlet The gauntlet object that contains the stepExecutor. + * @param board The board parameter to be passed to the stageSteps method. + */ + Closure getCls(){ + return { gauntlet, board -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } + + /** + * Executes the steps for the RecoverBoard stage. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board for which the BOOT files are being updated. + */ + void stageSteps(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running ${getStageName()} for ${board}") + def ref_branch = [] + def nebula_cmd = 'manager.recovery-device-manager --board-name=' + board + ' --folder=outs' + switch(gauntEnv.recovery_ref){ + case "SD": + nebula_cmd = nebula_cmd + ' --sdcard' + ref_branch = 'release' + break; + case "boot_partition_master": + ref_branch = 'master' + break; + case "boot_partition_release": + ref_branch = 'release' + break; + default: + throw new Exception('Unknown recovery ref branch: ' + gauntEnv.recovery_ref) + } + if (board=="pluto"){ + logger.warning("Recover stage does not support pluto yet!") + }else{ + if (gauntEnv.bootfile_source == "NA") + throw new Exception("bootfile_source must be specified") + + // confirm if indeed the board is dead and needs recovery + def to_proceed = false + try{ + gauntlet.nebula('net.check-board-booted --board-name=' + board) + logger.info('Board is booted, no need for recovery') + }catch(Exception ex){ + to_proceed = true + } + if(to_proceed){ + try{ + logger.info("Fetching reference boot files") + gauntlet.nebula('dl.bootfiles --board-name=' + board + + ' --source-root="' + gauntEnv.nebula_local_fs_source_root + + '" --source=' + gauntEnv.bootfile_source + + ' --branch="' + ref_branch.toString() + + '" --filetype="boot_partition"', true, true, true) + + logger.info("Extracting reference fsbl and u-boot") + steps.sh("cp outs/bootgen_sysfiles.tgz .") + steps.sh("tar -xzvf bootgen_sysfiles.tgz; cp u-boot*.elf u-boot.elf") + logger.info("Executing board recovery...") + gauntlet.nebula(nebula_cmd) + }catch(Exception ex){ + if(gauntEnv.netbox_allow_disable){ + def message = "Disabled by ${gauntEnv.env.JOB_NAME} ${gauntEnv.env.BUILD_NUMBER}" + def disable_command = "netbox.disable-board --netbox-ip=" + gauntEnv.netbox_ip + " --netbox-token=" + gauntEnv.netbox_token + " --board-name=" + board + " --failure --reason=" + "\"" + message + "\"" + " --power-off" + gauntlet.nebula(disable_command) + } + logger.error(gauntlet.getStackTrace(ex)) + throw ex + }finally{ + //archive uart logs + gauntlet.run_i("if [ -f recovery/${board}.log ]; then mv recovery/${board}.log uart_recover_" + board + ".log; fi") + steps.archiveArtifacts artifacts: 'uart_recover_*.log', followSymlinks: false, allowEmptyArchive: true + } + } + } + } +} diff --git a/src/sdg/stages/SendResults.groovy b/src/sdg/stages/SendResults.groovy new file mode 100644 index 00000000..bab9c8b8 --- /dev/null +++ b/src/sdg/stages/SendResults.groovy @@ -0,0 +1,79 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage + +/** + * The SendResults class implements the IStage interface and provides + * functionality to send results of a stage execution. + */ +class SendResults implements IStage { + /** + * Returns the name of the stage. + * + * @return The name of the stage, which is "SendResults". + */ + String getStageName(){ + return "SendResults" + } + + + Closure getCls(){ + return { gauntlet, board -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } + + void stageSteps(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running ${getStageName()} for ${board}") + def is_hdl_release = "False" + def is_linux_release = "False" + def is_boot_partition_release = "False" + def cmd = "" + if (gauntEnv.bootPartitionBranch == 'NA'){ + is_hdl_release = ( gauntEnv.hdlBranch == "release" )? "True": "False" + is_linux_release = ( gauntEnv.linuxBranch == "release" )? "True": "False" + }else{ + is_boot_partition_release = ( gauntEnv.bootPartitionBranch == "release" )? "True": "False" + } + steps.println(gauntEnv.elastic_logs) + logger.info("Starting send log to elastic search") + cmd = 'boot_folder_name ' + board + cmd += ' hdl_hash ' + '\'' + gauntlet.get_elastic_field(board, 'hdl_hash' , 'NA') + '\'' + cmd += ' linux_hash ' + '\'' + gauntlet.get_elastic_field(board, 'linux_hash' , 'NA') + '\'' + cmd += ' boot_partition_hash ' + '\'' + gauntEnv.boot_partition_hash + '\'' + cmd += ' hdl_branch ' + gauntEnv.hdlBranch + cmd += ' linux_branch ' + gauntEnv.linuxBranch + cmd += ' boot_partition_branch ' + gauntEnv.bootPartitionBranch + cmd += ' is_hdl_release ' + is_hdl_release + cmd += ' is_linux_release ' + is_linux_release + cmd += ' is_boot_partition_release ' + is_boot_partition_release + cmd += ' uboot_reached ' + gauntlet.get_elastic_field(board, 'uboot_reached', 'False') + cmd += ' linux_prompt_reached ' + gauntlet.get_elastic_field(board, 'linux_prompt_reached', 'False') + cmd += ' drivers_enumerated ' + gauntlet.get_elastic_field(board, 'drivers_enumerated', '0') + cmd += ' drivers_missing ' + gauntlet.get_elastic_field(board, 'drivers_missing', '0') + cmd += ' dmesg_warnings_found ' + gauntlet.get_elastic_field(board, 'dmesg_warns' , '0') + cmd += ' dmesg_errors_found ' + gauntlet.get_elastic_field(board, 'dmesg_errs' , '0') + // cmd +="jenkins_job_date datetime.datetime.now(), + cmd += ' jenkins_build_number ' + steps.getEnv().BUILD_NUMBER + cmd += ' jenkins_project_name ' + '\'' + steps.getEnv().JOB_NAME + '\'' + cmd += ' jenkins_agent ' + steps.getEnv().NODE_NAME + cmd += ' jenkins_trigger ' + gauntEnv.job_trigger + cmd += ' pytest_errors ' + gauntlet.get_elastic_field(board, 'pytest_errors', '0') + cmd += ' pytest_failures ' + gauntlet.get_elastic_field(board, 'pytest_failures', '0') + cmd += ' pytest_skipped ' + gauntlet.get_elastic_field(board, 'pytest_skipped', '0') + cmd += ' pytest_tests ' + gauntlet.get_elastic_field(board, 'pytest_tests', '0') + cmd += ' matlab_errors ' + gauntlet.get_elastic_field(board, 'matlab_errors', '0') + cmd += ' matlab_failures ' + gauntlet.get_elastic_field(board, 'matlab_failures', '0') + cmd += ' matlab_skipped ' + gauntlet.get_elastic_field(board, 'matlab_skipped', '0') + cmd += ' matlab_tests ' + gauntlet.get_elastic_field(board, 'matlab_tests', '0') + cmd += ' last_failing_stage ' + gauntlet.get_elastic_field(board, 'last_failing_stage', 'NA') + cmd += ' last_failing_stage_failure ' + gauntlet.get_elastic_field(board, 'last_failing_stage_failure', 'NA') + gauntlet.sendLogsToElastic(cmd) + } +} \ No newline at end of file diff --git a/src/sdg/stages/SimplyPrint.groovy b/src/sdg/stages/SimplyPrint.groovy new file mode 100644 index 00000000..9e4bd62f --- /dev/null +++ b/src/sdg/stages/SimplyPrint.groovy @@ -0,0 +1,51 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage + +/** + * The SimplyPrint class implements the IStage interface and provides + * functionality to print messages at different log levels. + * This class is used to demonstrate the new code structure + * and will likely be a pattern to all classes that implements the JSL common stages. + */ +class SimplyPrint implements IStage { + + /** + * Returns the name of the stage. + * + * @return The name of the stage, which is "SimplyPrint". + */ + String getStageName(){ + return "SimplyPrint" + } + + /** + * Executes the stage steps, setting the debug level and logging messages + * at different levels. + * + * @param gauntlet The Gauntlet instance used to set environment variables and log messages. + * @param board The board name for which the stage is being executed. + */ + void stageSteps(Gauntlet gauntlet, String board){ + gauntlet.set_env("debug_level",2) + gauntlet.logger.info("Running from ${getStageName()} for ${board}") + gauntlet.logger.warning("Running from ${getStageName()} for ${board}") + gauntlet.logger.error("Running from ${getStageName()} for ${board}") + } + + /** + * Returns a closure that sets the debug level, logs an info message, and + * executes the stage steps within a stage block. + * + * @return A closure that takes a Gauntlet instance and a board name as parameters. + */ + Closure getCls(){ + return { gauntlet, board -> + gauntlet.set_env("debug_level",3) + gauntlet.logger.info("Running from ${getStageName()} for ${board}") + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } +} diff --git a/src/sdg/stages/UpdateBOOTFiles.groovy b/src/sdg/stages/UpdateBOOTFiles.groovy new file mode 100644 index 00000000..e66780fe --- /dev/null +++ b/src/sdg/stages/UpdateBOOTFiles.groovy @@ -0,0 +1,193 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage +import sdg.NominalException +import org.jenkinsci.plugins.pipeline.modeldefinition.Utils + +/** + * This class represents the "UpdateBOOTFiles" stage in the Jenkins pipeline. + * It contains methods related to updating BOOT files. + */ +class UpdateBOOTFiles implements IStage { + + /** + * Retrieves the name of the stage. + * + * @return A string representing the name of the stage, which is "UpdateBOOTFiles". + */ + String getStageName(){ + return "UpdateBOOTFiles" + } + + /** + * Returns a closure that executes a stage in the Jenkins pipeline. + * + * @return Closure that takes three parameters: gauntlet, board, and an optional ml_bootbin_case. + * The closure executes a stage using the gauntlet's stepExecutor and calls the stageSteps method. + * + * @param gauntlet The gauntlet object that contains the stepExecutor. + * @param board The board parameter to be passed to the stageSteps method. + * @param ml_bootbin_case Optional parameter to be passed to the stageSteps method. + */ + Closure getCls(){ + return { gauntlet, board, ml_bootbin_case=null -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board, ml_bootbin_case) + } + } + } + + /** + * Executes the steps for the UpdateBOOTFiles stage. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board for which the BOOT files are being updated. + * @param ml_bootbin_case The case identifier for the ML boot binary. + */ + void stageSteps(Gauntlet gauntlet, String board, String ml_bootbin_case){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + logger.info("Running ${getStageName()} for ${board}") + try{ + def boolean trxPluto = gauntEnv.docker_args.contains("MATLAB") && (board=="pluto") + if (trxPluto){ + logger.info("Skip pluto firmware update.") + Utils.markStageSkippedForConditional(getStageName()) + }else{ + logger.info("Board name passed: "+board) + logger.info("Branch: " + gauntEnv.branches.toString()) + try{ + if (gauntEnv.toolbox_generated_bootbin) { + logger.info("MATLAB BOOT.BIN job variation: "+ml_bootbin_case) + logger.info("Downloading bootbin generated from toolbox") + gauntlet.nebula('show-log dl.matlab-bootbins'+ + ' -t "'+gauntEnv.ml_toolbox+ + '" -b "'+gauntEnv.ml_branch+ + '" -u "'+gauntEnv.ml_build+'"') + } + if (board=="pluto"){ + if (gauntEnv.firmwareVersion == 'NA') + throw new Exception("Firmware must be specified") + gauntlet.nebula('dl.bootfiles --board-name=' + board + + ' --source="github"' + + ' --branch="' + gauntEnv.firmwareVersion + + '" --filetype="firmware"', true, true, true) + }else{ + if (gauntEnv.branches == ["NA","NA"]) + throw new Exception("Either hdl_branch/linux_branch or boot_partition_branch must be specified") + if (gauntEnv.bootfile_source == "NA") + throw new Exception("bootfile_source must be specified") + def cmd = 'dl.bootfiles --board-name=' + board + cmd += ' --source-root=' + gauntEnv.nebula_local_fs_source_root + cmd += ' --source=' + gauntEnv.bootfile_source + cmd += ' --branch=' + gauntEnv.branches.toString() + cmd += (gauntEnv.url_template == 'NA')? "" : ' --url-template=' + gauntEnv.url_template + cmd += gauntEnv.filetype + gauntlet.nebula(cmd, true, true, true) + } + //get git sha properties of files + gauntlet.get_gitsha(board) + }catch(Exception ex){ + throw new Exception('Downloader error: '+ ex.getMessage()) + } + + if(gauntEnv.toolbox_generated_bootbin) { + logger.info("Replace bootbin with one generated from toolbox") + // Get list of files in ml_bootbins folder + def ml_bootfiles = sh (script: "ls ml_bootbins", returnStdout: true).trim() + logger.info("ml_bootfiles: " + ml_bootfiles) + // Filter bootbin for specific case (rx,tx,rxtx) + def found = false; + for (String bootfile : ml_bootfiles.split("\\r?\\n")) { + logger.info("Inspecting " + bootfile + " for " + ml_bootbin_case + "_BOOT.BIN") + logger.info("Must contain board: " + board) + logger.info(bootfile.contains(board) && bootfile.contains("_"+ml_bootbin_case+"_BOOT.BIN")) + if (bootfile.contains(board) && bootfile.contains("_"+ml_bootbin_case+"_BOOT.BIN")) { + // Copy bootbin to outs folder + logger.info("Copy " + bootfile + " to outs folder") + sh "cp ml_bootbins/${bootfile} outs/BOOT.BIN" + found = true; + break + } + } + if (!found) { + logger.info("No bootbin found for " + ml_bootbin_case + " case") + logger.info("Skipping Update BOOT Files stage") + logger.info("Skipping "+gauntEnv.ml_test_stages.toString()+" related test stages") + gauntEnv.internal_stages_to_skip[board] = gauntEnv.ml_test_stages; + return; + } + } + + //update-boot-files + gauntlet.nebula('manager.update-boot-files --board-name=' + board + ' --folder=outs', true, true, true) + if (board=="pluto"){ + gauntlet.stepExecutor.retry(2){ + steps.sleep(50) + gauntlet.nebula('uart.set-local-nic-ip-from-usbdev --board-name=' + board) + } + } + + gauntlet.set_elastic_field(board, 'uboot_reached', 'True') + gauntlet.set_elastic_field(board, 'kernel_started', 'True') + gauntlet.set_elastic_field(board, 'linux_prompt_reached', 'True') + gauntlet.set_elastic_field(board, 'post_boot_failure', 'False') + + // verify checksum + if (board == "pluto"){ + logger.info("Skipping checksum verification.") + }else { + gauntlet.nebula('manager.verify-checksum --board-name=' + board + ' --folder=outs', true, true, true) + } + } + }catch(Exception ex){ + + def is_nominal_exception = false + if (ex.getMessage().contains('u-boot not reached')){ + gauntlet.set_elastic_field(board, 'uboot_reached', 'False') + gauntlet.set_elastic_field(board, 'kernel_started', 'False') + gauntlet.set_elastic_field(board, 'linux_prompt_reached', 'False') + }else if (ex.getMessage().contains('u-boot menu cannot boot kernel')){ + gauntlet.set_elastic_field(board, 'uboot_reached', 'True') + gauntlet.set_elastic_field(board, 'kernel_started', 'False') + gauntlet.set_elastic_field(board, 'linux_prompt_reached', 'False') + }else if (ex.getMessage().contains('Linux not fully booting')){ + gauntlet.set_elastic_field(board, 'uboot_reached', 'True') + gauntlet.set_elastic_field(board, 'kernel_started', 'True') + gauntlet.set_elastic_field(board, 'linux_prompt_reached', 'False') + }else if (ex.getMessage().contains('Linux is functional but Ethernet is broken after updating boot files') || + ex.getMessage().contains('SSH not working but ping does after updating boot files') || + ex.getMessage().contains('Checksum does not match')){ + gauntlet.set_elastic_field(board, 'uboot_reached', 'True') + gauntlet.set_elastic_field(board, 'kernel_started', 'True') + gauntlet.set_elastic_field(board, 'linux_prompt_reached', 'True') + gauntlet.set_elastic_field(board, 'post_boot_failure', 'True') + }else if (ex.getMessage().contains('Downloader error')){ + gauntlet.set_elastic_field(board, 'uboot_reached', 'False') + gauntlet.set_elastic_field(board, 'kernel_started', 'False') + gauntlet.set_elastic_field(board, 'linux_prompt_reached', 'False') + is_nominal_exception = true + }else{ + logger.error("Update BOOT Files unexpectedly failed. ${ex.getMessage()}") + } + gauntlet.get_gitsha(board) + def failing_msg = "'" + ex.getMessage().split('\n').last().replaceAll( /(['])/, '"') + "'" + // send logs to elastic + if (gauntEnv.send_results){ + gauntlet.set_elastic_field(board, 'last_failing_stage', 'UpdateBOOTFiles') + gauntlet.set_elastic_field(board, 'last_failing_stage_failure', failing_msg) + stage_library('SendResults').call(this, board) + } + if (is_nominal_exception) + throw new NominalException('UpdateBOOTFiles failed: '+ ex.getMessage()) + throw new Exception('UpdateBOOTFiles failed: '+ ex.getMessage()) + + }finally{ + + //archive uart logs + gauntlet.run_i("if [ -f ${board}.log ]; then mv ${board}.log uart_boot_" + board + ".log; fi") + gauntlet.stepExecutor.archiveArtifacts artifacts: 'uart_boot_*.log', followSymlinks: false, allowEmptyArchive: true + + } + } +} diff --git a/src/sdg/stages/noOSTest.groovy b/src/sdg/stages/noOSTest.groovy new file mode 100644 index 00000000..8cd349d0 --- /dev/null +++ b/src/sdg/stages/noOSTest.groovy @@ -0,0 +1,170 @@ +package sdg.stages +import sdg.Gauntlet +import sdg.stages.IStage +import sdg.NominalException + +/** + * This class represents the "noOSTest" stage in the Jenkins pipeline. + * It contains methods related to running tests on the board without an OS. + */ +class noOSTest implements IStage { + + /** + * Retrieves the name of the stage. + * + * @return A string representing the name of the stage, which is "noOSTest". + */ + String getStageName(){ + return "noOSTest" + } + + /** + * Returns a closure that executes a stage in the Jenkins pipeline. + * + * @return Closure that takes two parameters: gauntlet and board + * The closure executes a stage using the gauntlet's stepExecutor and calls the stageSteps method. + * + * @param gauntlet The gauntlet object that contains the stepExecutor. + * @param board The board parameter to be passed to the stageSteps method. + */ + Closure getCls(){ + return { gauntlet, board -> + gauntlet.stepExecutor.stage(getStageName()){ + stageSteps(gauntlet, board) + } + } + } + + /** + * Executes the steps for the noOSTest stage. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board on which the tests are being run. + */ + void stageSteps(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running ${getStageName()} for ${board}") + steps.stage("Run Tests"){ + RunTests(gauntlet, board, DownloadBinaries(gauntlet, board)) + } + } + + /** + * Downloads the binaries for the board. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board for which the binaries are being downloaded. + * @return The filepath of the downloaded binaries. + */ + String DownloadBinaries(Gauntlet gauntlet, String board){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + def example = gauntlet.nebula('update-config board-config example --board-name='+board) + def platform = gauntlet.nebula('update-config downloader-config platform --board-name='+board) + def filepath = '' + logger.info("Downloading binaries for ${board}") + gauntlet.nebula('dl.bootfiles --board-name=' + board + ' --source-root="' + gauntEnv.nebula_local_fs_source_root + '" --source=' + gauntEnv.bootfile_source + + ' --branch="' + gauntEnv.hdlBranch.toString() + '" --filetype="noos"', true, true, true) + def binaryfiles = steps.sh (script: "ls outs", returnStdout: true).trim() + logger.info("binary files: " + binaryfiles) + def found = false; + for (String binaryfile : binaryfiles.split("\\r?\\n")) { + def carrier = board.split('_')[0] + def daughter = board.split('_')[1] + if (daughter.contains('-')){ + daughter = daughter.split('-')[0] + } + if (binaryfile.contains(example) && binaryfile.contains(carrier) && binaryfile.contains(daughter)){ + if (platform == "Xilinx"){ + def bootgen = 'outs/'+binaryfile+'/bootgen_sysfiles.tar.gz' + steps.sh(script: 'tar -xf '+bootgen) + filepath = steps.sh(script: 'ls | grep *'+carrier+'.elf', returnStdout: true).trim() + logger.info("File/filepath: "+filepath) + found = true; + break + }else { + if (binaryfile.contains('.elf')) { + filepath = 'outs/'+binaryfile + logger.info("File/filepath: "+filepath) + found = true; + break + } + } + } + } + if (!found) { + //for now, stop test pipeline if file is not found + throw new Exception("No elf found for "+board) + } + return filepath + } + + /** + * Runs the tests on the board. + * + * @param gauntlet The Gauntlet instance used to execute the stage. + * @param board The name of the board on which the tests are being run. + * @param filepath The filepath of the downloaded binaries. + */ + void RunTests(Gauntlet gauntlet, String board, String filepath){ + def logger = gauntlet.logger + def gauntEnv = gauntlet.gauntEnv + def steps = gauntlet.stepExecutor + + logger.info("Running tests for ${board}") + def project = gauntlet.nebula('update-config downloader-config no_os_project --board-name='+board) + def jtag_cable_id = gauntlet.nebula('update-config jtag-config jtag_cable_id --board-name='+board) + def serial = gauntlet.nebula('update-config uart-config address --board-name='+board) + def baudrate = gauntlet.nebula('update-config uart-config baudrate --board-name='+board) + def platform = gauntlet.nebula('update-config downloader-config platform --board-name='+board) + def example = gauntlet.nebula('update-config board-config example --board-name='+board) + def screen_baudrate + + if (gauntEnv.vivado_ver == '2020.1' || gauntEnv.vivado_ver == '2021.1' ){ + steps.sh 'ln /usr/bin/make /usr/bin/gmake' + } + if (example.contains('iio')){ + screen_baudrate = gauntEnv.iio_uri_baudrate + } else { + screen_baudrate = baudrate + } + steps.sh 'screen -S ' +board+ ' -dm -L -Logfile ' +board+'-boot.log ' +serial+ ' '+screen_baudrate + if (platform == "Xilinx"){ + steps.sh 'git clone --depth=1 -b '+gauntEnv.no_os_branch+' '+gauntEnv.no_os_repo + steps.sh 'cp '+filepath+ ' no-OS/projects/'+ project +'/' + steps.sh 'cp *.xsa no-OS/projects/'+ project +'/system_top.xsa' + steps.dir('no-OS/projects/'+project){ + steps.sh 'source /opt/Xilinx/Vivado/' +gauntEnv.vivado_ver+ '/settings64.sh && make run' +' JTAG_CABLE_ID='+jtag_cable_id + } + } else { + gauntlet.run_i('wget https://raw.githubusercontent.com/analogdevicesinc/no-OS/'+gauntEnv.no_os_branch+'/tools/scripts/mcufla.sh', true) + steps.sh 'chmod +x mcufla.sh' + def cmd = './mcufla.sh ' +filepath+' '+jtag_cable_id + def flashStatus = steps.sh (returnStatus: true, script: cmd) + if ((flashStatus != 0)){ + throw new Exception("Flashing binary file failed.") + } + } + steps.sleep(180) //wait to fully boot + steps.archiveArtifacts artifacts: "*-boot.log", followSymlinks: false, allowEmptyArchive: true + steps.sh 'screen -XS '+board+ ' kill' + if (example.contains('iio')){ + steps.retry(3){ + logger.info("---------------------------") + steps.sleep(10); + logger.info("Check context") + def cmd = 'iio_info -u serial:' + serial + ',' +baudrate+ ' &> '+board+'-iio_info.log' + def ret = steps.sh (returnStatus: true, script: cmd) + steps.archiveArtifacts artifacts: "*-iio_info.log", followSymlinks: false, allowEmptyArchive: true + if (ret != 0){ + throw new Exception("Failed.") + } + } + } + } +} \ No newline at end of file diff --git a/test/resources/nebula_failures/pluto_iio_devices_out.out b/test/resources/nebula_failures/pluto_iio_devices_out.out new file mode 100644 index 00000000..151a3b48 --- /dev/null +++ b/test/resources/nebula_failures/pluto_iio_devices_out.out @@ -0,0 +1,7 @@ +INFO | nebula.common : Depth of config: 2 +INFO | nebula.common : board_name used: pluto +INFO | nebula.driver : Checking uri: ip:localhost +INFO | nebula.driver : Checking for: adm1177-iio +INFO | nebula.driver : Checking for: ad9361-phy +INFO | nebula.driver : Checking for: cf-ad9361-dds-core-lpc +INFO | nebula.driver : Checking for: cf-ad9361-lpc diff --git a/test/resources/nebula_failures/pluto_iio_devices_out_fail.out b/test/resources/nebula_failures/pluto_iio_devices_out_fail.out new file mode 100644 index 00000000..4b8c1f9f --- /dev/null +++ b/test/resources/nebula_failures/pluto_iio_devices_out_fail.out @@ -0,0 +1,24 @@ +INFO | nebula.common : Depth of config: 2 +INFO | nebula.common : board_name used: pluto +INFO | nebula.driver : Checking uri: ip:localhost +INFO | nebula.driver : Checking for: adm1177-iio +INFO | nebula.driver : Checking for: ad9361-phy +INFO | nebula.driver : Checking for: cf-ad9361-dds-core-lpc +INFO | nebula.driver : Checking for: cf-ad9361-lpc +INFO | nebula.driver : Checking for: axi-dummy +Traceback (most recent call last): + File "/home/analog/.local/bin/nebula", line 8, in + sys.exit(program.run()) + File "/usr/local/lib/python3.10/dist-packages/invoke/program.py", line 384, in run + self.execute() + File "/usr/local/lib/python3.10/dist-packages/invoke/program.py", line 569, in execute + executor.execute(*self.tasks) + File "/usr/local/lib/python3.10/dist-packages/invoke/executor.py", line 129, in execute + result = call.task(*args, **call.kwargs) + File "/usr/local/lib/python3.10/dist-packages/invoke/tasks.py", line 127, in __call__ + result = self.body(*args, **kwargs) + File "/home/analog/.local/lib/python3.10/site-packages/nebula/tasks.py", line 363, in check_iio_devices + d.check_iio_devices() + File "/home/analog/.local/lib/python3.10/site-packages/nebula/driver.py", line 52, in check_iio_devices + raise Exception("Device(s) not found " + str(missing_devs)) +Exception: Device(s) not found ['axi-dummy'] \ No newline at end of file diff --git a/test/sdg/stages/TestCaptureIIOContext.groovy b/test/sdg/stages/TestCaptureIIOContext.groovy new file mode 100644 index 00000000..488b2d18 --- /dev/null +++ b/test/sdg/stages/TestCaptureIIOContext.groovy @@ -0,0 +1,74 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestCaptureIIOContext extends Specification { + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class) + context = Mock(IContext.class) + } + + def "test getStageName"() { + given: + def CaptureIIOContext = new CaptureIIOContext() + + expect: + CaptureIIOContext.getStageName() == "CaptureIIOContext" + } + + def "test getCls"() { + given: + def CaptureIIOContext = new CaptureIIOContext() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + when: + def closure = CaptureIIOContext.getCls() + + then: + closure instanceof Closure + } + + def "test stageSteps - capture IIO context"() { + given: + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","NA","v0.31","NA") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","NA") + + def CaptureIIOContext = new CaptureIIOContext() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + when: + CaptureIIOContext.stageSteps(gauntlet, board) + + then: + 1 * steps.sh('git clone https://github.com/analogdevicesinc/libtinyiiod.git') + 1 * steps.sh('git clone -b v0.1.0 https://github.com/analogdevicesinc/iio-emu.git') + 1 * steps.sh(['script':'nebula update-config network-config dutip --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', 'returnStdout':true]) + 1 * steps.sh('xml_gen ip: > "zynq-zc702-adv7511-ad9361-fmcomms2-3.xml"') + 1 * steps.archiveArtifacts(['artifacts':'*.xml']) + + } +} \ No newline at end of file diff --git a/test/sdg/stages/TestKuiperCheck.groovy b/test/sdg/stages/TestKuiperCheck.groovy new file mode 100644 index 00000000..f041e3a5 --- /dev/null +++ b/test/sdg/stages/TestKuiperCheck.groovy @@ -0,0 +1,138 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestKuiperCheck extends Specification { + + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class) + context = Mock(IContext.class) + } + + def "test getStageName"() { + given: + def kuiperCheck = new KuiperCheck() + + expect: + kuiperCheck.getStageName() == "KuiperCheck" + } + + def "test getCls"() { + given: + def kuiperCheck = new KuiperCheck() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + when: + def closure = kuiperCheck.getCls() + + then: + closure instanceof Closure + } + + def "test stageSteps - KPBC run"() { + given: + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + + // Create gauntEnv with required properties + def mockGauntEnv = getGauntEnv.call("NA", "NA", "NA", "v0.31", "NA") + mockGauntEnv.kuiper_checker_branch = "master" + mockGauntEnv.kuiper_checker_repo = "https://github.com/sdgtt/kuiper-post-build-checker.git" + + steps.getGauntEnv(_, _, _, _, _) >> mockGauntEnv + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + + // Mock all shell commands that will be called + steps.sh(_) >> { args -> + if (args instanceof Map) { + if (args.returnStdout == true && args.script?.contains('nebula update-config')) { + return "192.168.1.100" // Return IP for nebula commands + } else if (args.returnStatus == true) { + return 0 // Return success status code for pytest + } + } + return 0 // Default return + } + + // Mock dir operations with closure execution + steps.dir(_, _) >> { String dirName, Closure closure -> + closure.call() + } + + // Mock other Jenkins pipeline steps + steps.fileExists(_) >> true + steps.junit({ Map params -> params.testResults && params.allowEmptyResults != null }) >> null + steps.publishHTML(_) >> null + steps.stage(_, _) >> { String stageName, Closure closure -> + closure.call() + } + + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA", "NA", "NA", "v0.31", "NA") + + // Ensure gauntlet has the correct gauntEnv + gauntlet.gauntEnv = mockGauntEnv + + // Mock the logger + def mockLogger = Mock(sdg.Logger) + gauntlet.logger = mockLogger + + // Mock gauntlet methods using metaClass + gauntlet.metaClass.run_i = { String cmd, boolean doRetry = false -> + return null // Mock successful execution + } + gauntlet.metaClass.nebula = { String cmd -> + return "192.168.1.100" // Mock IP address return + } + + def KuiperCheck = new KuiperCheck() + + when: + def exceptionThrown = false + try { + KuiperCheck.stageSteps(gauntlet, board) + } catch (Exception e) { + exceptionThrown = true + // We expect a NominalException due to status code, that's okay for this test + } + + then: + // Verify the logger was called + 1 * mockLogger.info('Running KuiperCheck for zynq-zc702-adv7511-ad9361-fmcomms2-3') + + // Verify the pytest command was executed with the expected structure + 1 * steps.sh({ Map params -> + params.script && + params.script.contains('python3 -m pytest') && + params.script.contains('--html=testhtml/') && + params.script.contains('--junitxml=testxml/') && + params.script.contains('--ip=') && + params.script.contains('not hardware_check') && + params.returnStatus == true + }) + + exceptionThrown == true // We expect an exception due to pytest failure + } +} \ No newline at end of file diff --git a/test/sdg/stages/TestLibAD9361Tests.groovy b/test/sdg/stages/TestLibAD9361Tests.groovy new file mode 100644 index 00000000..a83882a0 --- /dev/null +++ b/test/sdg/stages/TestLibAD9361Tests.groovy @@ -0,0 +1,138 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestLibAD9361Tests extends Specification { + + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class) + context = Mock(IContext.class) + } + + def "test getStageName"() { + given: + def libAD9361Tests = new LibAD9361Tests() + + expect: + libAD9361Tests.getStageName() == "LibAD9361Tests" + } + + def "test getCls"() { + given: + def libAD9361Tests = new LibAD9361Tests() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + when: + def closure = libAD9361Tests.getCls() + + then: + closure instanceof Closure + } + + def "test stageSteps - supported board"() { + given: + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + + // Create gauntEnv with required properties + def mockGauntEnv = getGauntEnv.call("NA","NA","NA","v0.31","NA") + mockGauntEnv.libad9361_iio_branch = "main" + mockGauntEnv.libad9361_iio_repo = "https://github.com/analogdevicesinc/libad9361-iio.git" + + steps.getGauntEnv(_,_,_,_,_) >> mockGauntEnv + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + + // Mock all shell commands that will be called + steps.sh('mkdir -p build') >> null + steps.sh('cmake -DPYTHON_BINDINGS=ON ..') >> null + steps.sh('make') >> null + steps.sh('make install') >> null + steps.sh('ldconfig') >> null + steps.sh({ String cmd -> cmd.contains('URI_AD9361') && cmd.contains('ctest') }) >> null + steps.sh("mv Testing ${board}") >> null + + // Mock dir operations with closure execution + steps.dir(_, _) >> { String dirName, Closure closure -> + closure.call() + } + + // Mock other Jenkins pipeline steps + steps.unstable(_) >> null + steps.archiveArtifacts(_) >> null + steps.xunit(_) >> null + steps.CTest(_) >> { Map params -> params } + + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","v0.31","NA") + + // Mock gauntlet methods using metaClass + gauntlet.metaClass.run_i = { String cmd, boolean doRetry = false -> + return null // Mock successful execution + } + + def libAD9361Tests = new LibAD9361Tests() + + when: + libAD9361Tests.stageSteps(gauntlet, board) + + then: + 1 * steps.sh(['script':'nebula update-config -s network-config -f dutip --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', 'returnStdout':true]) + 1 * steps.sh('URI_AD9361="ip:" ctest -T test --no-compress-output -V') // Verify test command is called + } + + def "test stageSteps - unsupported board"() { + given: + String board = "zynq-zc706-adv7511-fmcomms11" + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + + // Create gauntEnv - for unsupported board test, we can use basic gauntEnv + def mockGauntEnv = getGauntEnv.call("NA","NA","NA","v0.31","NA") + + steps.getGauntEnv(_,_,_,_,_) >> mockGauntEnv + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","v0.31","NA") + + // Mock the logger + def logger = Mock(sdg.Logger) + gauntlet.logger = logger + + def libAD9361Tests = new LibAD9361Tests() + + when: + libAD9361Tests.stageSteps(gauntlet, board) + + then: + 1 * logger.info("LibAD9361Tests: Skipping board: "+board) + } +} \ No newline at end of file diff --git a/test/sdg/stages/TestLinuxTests.groovy b/test/sdg/stages/TestLinuxTests.groovy new file mode 100644 index 00000000..a97b2f81 --- /dev/null +++ b/test/sdg/stages/TestLinuxTests.groovy @@ -0,0 +1,244 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestLinuxTests extends Specification { + + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class) + context = Mock(IContext.class) + } + + def "test getStageName"() { + given: + LinuxTests linuxTestsStage = new LinuxTests() + + expect: + linuxTestsStage.getStageName() == "LinuxTests" + } + + def "test getCls"() { + given: + LinuxTests linuxTestsStage = new LinuxTests() + String board = "pluto" + + when: + def closure = linuxTestsStage.getCls() + + then: + closure instanceof Closure + } + + def "test stageSteps"(){ + given: + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","NA","v0.31","NA") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","NA") + + LinuxTests linuxTestsStage = new LinuxTests() + String board = "pluto" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + + steps.sh(script: 'cat dmesg_err_filtered.log | wc -l', returnStdout: true) >> "0" + steps.sh(script: 'cat dmesg_warn.log | wc -l', returnStdout: true) >> "0" + steps.sh(script: 'nebula show-log update-config driver-config iio_device_names -b pluto', + returnStdout: true) >> '["adm1177-iio","ad9361-phy","cf-ad9361-dds-core-lpc","cf-ad9361-lpc"]' + + when: + linuxTestsStage.stageSteps(gauntlet, board) + + then: + 1 * steps.echo('[INFO] Running LinuxTests for ' + board) + 1 * steps.sh('set -o pipefail; nebula show-log driver.check-iio-devices --uri="ip:" --board-name=pluto 2>&1 | tee out.out') + assert gauntlet.get_elastic_field("pluto", "drivers_enumerated") == "4" + assert gauntlet.get_elastic_field("pluto", "dmesg_errs") == "0" + assert gauntlet.get_elastic_field("pluto", "dmesg_warns") == "0" + } + + def "test stageSteps - missing devices"(){ + given: + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","NA","v0.31","NA") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> new File('test/resources/nebula_failures/pluto_iio_devices_out_fail.out').text + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","NA") + + LinuxTests linuxTestsStage = new LinuxTests() + String board = "pluto" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + + steps.sh(script: 'cat dmesg_err_filtered.log | wc -l', returnStdout: true) >> "0" + steps.sh(script: 'cat dmesg_warn.log | wc -l', returnStdout: true) >> "0" + steps.sh('set -o pipefail; nebula show-log driver.check-iio-devices --uri="ip:" --board-name=pluto 2>&1 | tee out.out') >> { throw new Exception() } + steps.sh(script: 'nebula show-log update-config driver-config iio_device_names -b pluto', + returnStdout: true) >> '["adm1177-iio","ad9361-phy","cf-ad9361-dds-core-lpc","cf-ad9361-lpc"]' + + + when: + linuxTestsStage.stageSteps(gauntlet, board) + + then: + 1 * steps.echo('[INFO] Running LinuxTests for ' + board) + assert gauntlet.get_elastic_field("pluto", "drivers_enumerated") == "4" + assert gauntlet.get_elastic_field("pluto", "drivers_missing") == "1" + assert gauntlet.get_elastic_field("pluto", "dmesg_errs") == "0" + assert gauntlet.get_elastic_field("pluto", "dmesg_warns") == "0" + } + + + def "test stageSteps - non-pluto"(){ + given: + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + def mockGauntEnv = getGauntEnv.call("NA", "NA", "NA", "v0.31", "NA") + mockGauntEnv.test_adi_diagnostics = true + steps.getGauntEnv(_,_,_,_,_) >> mockGauntEnv + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","artifactory") + + LinuxTests linuxTestsStage = new LinuxTests() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + + steps.sh(script: 'cat dmesg_err_filtered.log | wc -l', returnStdout: true) >> "0" + steps.sh(script: 'cat dmesg_warn.log | wc -l', returnStdout: true) >> "0" + steps.sh(script: 'nebula show-log update-config driver-config iio_device_names -b zynq-zc702-adv7511-ad9361-fmcomms2-3', + returnStdout: true) >> '["ad7291","ad9361-phy","cf-ad9361-dds-core-lpc","cf-ad9361-lpc"]' + steps.sh(['script':'nebula update-config board-config serial --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', + 'returnStdout':true]) >> { throw new Exception() } + + when: + linuxTestsStage.stageSteps(gauntlet, board) + + then: + 1 * steps.echo('[INFO] Running LinuxTests for ' + board) + 1 * steps.sh('set -o pipefail; nebula show-log driver.check-iio-devices --uri="ip:" --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3 2>&1 | tee out.out') + 1 * steps.sh('set -o pipefail; nebula show-log net.run-diagnostics --ip=\'\' --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3 2>&1 | tee out.out') + + assert gauntlet.get_elastic_field("zynq-zc702-adv7511-ad9361-fmcomms2-3", "drivers_enumerated") == "4" + assert gauntlet.get_elastic_field("zynq-zc702-adv7511-ad9361-fmcomms2-3", "dmesg_errs") == "0" + assert gauntlet.get_elastic_field("zynq-zc702-adv7511-ad9361-fmcomms2-3", "dmesg_warns") == "0" + } + + def "test stageSteps - non-pluto w/ failed dmesg"(){ + given: + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","NA","NA","artifactory") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","artifactory") + + LinuxTests linuxTestsStage = new LinuxTests() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + + steps.sh(script: 'cat dmesg_err_filtered.log | wc -l', returnStdout: true) >> "1" + steps.sh(script: 'cat dmesg_warn.log | wc -l', returnStdout: true) >> "1" + steps.sh(script: 'nebula show-log update-config driver-config iio_device_names -b zynq-zc702-adv7511-ad9361-fmcomms2-3', + returnStdout: true) >> '["ad7291","ad9361-phy","cf-ad9361-dds-core-lpc","cf-ad9361-lpc"]' + steps.sh(['script':'nebula net.check-dmesg --ip=\'\' --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', + 'returnStdout':true]) >> { throw new Exception() } + steps.sh(['script':'nebula update-config board-config serial --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', + 'returnStdout':true]) >> { throw new Exception() } + + when: + linuxTestsStage.stageSteps(gauntlet, board) + + then: + assert gauntlet.get_elastic_field("zynq-zc702-adv7511-ad9361-fmcomms2-3", "dmesg_errs") == "1" + assert gauntlet.get_elastic_field("zynq-zc702-adv7511-ad9361-fmcomms2-3", "dmesg_warns") == "1" + 1 * steps.unstable('Linux Tests Failed: [dmesg check failed: null]') + } + + def "test stageSteps - non pluto w/ failed diagnostics"(){ + given: + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + def mockGauntEnv = getGauntEnv.call("NA", "NA", "NA", "v0.31", "NA") + mockGauntEnv.test_adi_diagnostics = true + steps.getGauntEnv(_,_,_,_,_) >> mockGauntEnv + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","artifactory") + + LinuxTests linuxTestsStage = new LinuxTests() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + + steps.sh(script: 'cat dmesg_err_filtered.log | wc -l', returnStdout: true) >> "0" + steps.sh(script: 'cat dmesg_warn.log | wc -l', returnStdout: true) >> "0" + steps.sh(script: 'nebula show-log update-config driver-config iio_device_names -b zynq-zc702-adv7511-ad9361-fmcomms2-3', + returnStdout: true) >> '["ad7291","ad9361-phy","cf-ad9361-dds-core-lpc","cf-ad9361-lpc"]' + steps.sh(['script':'nebula update-config board-config serial --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', + 'returnStdout':true]) >> { throw new Exception() } + steps.sh('set -o pipefail; nebula show-log net.run-diagnostics --ip=\'\' --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3 2>&1 | tee out.out') >> { throw new Exception() } + + when: + linuxTestsStage.stageSteps(gauntlet, board) + + then: + 1 * steps.echo('[INFO] Running LinuxTests for ' + board) + 1 * steps.sh('set -o pipefail; nebula show-log driver.check-iio-devices --uri="ip:" --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3 2>&1 | tee out.out') + 1 * steps.unstable('Linux Tests Failed: [diagnostics failed: nebula failed]') + + assert gauntlet.get_elastic_field("zynq-zc702-adv7511-ad9361-fmcomms2-3", "drivers_enumerated") == "4" + assert gauntlet.get_elastic_field("zynq-zc702-adv7511-ad9361-fmcomms2-3", "dmesg_errs") == "0" + assert gauntlet.get_elastic_field("zynq-zc702-adv7511-ad9361-fmcomms2-3", "dmesg_warns") == "0" + } +} \ No newline at end of file diff --git a/test/sdg/stages/TestMATLABTests.groovy b/test/sdg/stages/TestMATLABTests.groovy new file mode 100644 index 00000000..65b4203d --- /dev/null +++ b/test/sdg/stages/TestMATLABTests.groovy @@ -0,0 +1,191 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestMATLABTests extends Specification { + + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class) + context = Mock(IContext.class) + } + + def "test getStageName"() { + given: + def matlabTests = new MATLABTests() + + expect: + matlabTests.getStageName() == "MATLABTests" + } + + def "test getCls"() { + given: + def matlabTests = new MATLABTests() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + when: + def closure = matlabTests.getCls() + + then: + closure instanceof Closure + } + + def "test MATLABTests"() { + given: + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + // Create gauntEnv with required properties + def mockGauntEnv = getGauntEnv.call("NA","NA","NA","v0.31","NA") + mockGauntEnv.matlab_branch = "master" + mockGauntEnv.matlab_repo = "https://github.com/analogdevicesinc/TransceiverToolbox.git" + mockGauntEnv.matlab_release = "R2021a" + mockGauntEnv.matlab_timeout = "10m" + mockGauntEnv.elastic_server = "localhost:9200" + mockGauntEnv.matlab_commands = ["runHWTests('AD9361')"] + + steps.getGauntEnv(_,_,_,_,_) >> mockGauntEnv + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.fileExists({ it.contains('_HWTestResults.xml') || it.contains('Results.xml') }) >> false // No XML file exists for successful test + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + steps.retry(_, _) >> { int count, Closure cls -> cls.call() } + steps.checkout(_) >> null + steps.writeFile(_) >> null + steps.junit(_) >> null + steps.fileExists(_) >> true // Mock MATLAB executable exists + + // Mock the cp command for matlab setup + steps.sh('cp -r /root/.matlabro /root/.matlab') >> null + + // Mock nebula command for getting IP + steps.sh([script: 'nebula update-config network-config dutip --board-name=' + board, returnStdout: true]) >> '192.168.1.100' + + // Mock dir operations with closure execution + steps.dir(_, _) >> { String dirName, Closure closure -> + closure.call() + } + + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + + // Mock the getURIFromSerial method + gauntlet.metaClass.getURIFromSerial = { String boardName -> + return "serial:/dev/ttyACM0,115200" + } + + // Mock the isMultiBranchPipeline method + gauntlet.metaClass.isMultiBranchPipeline = { String repo -> + return [isMultiBranch: false, branch: "master", ref: "+refs/heads/master:refs/remotes/origin/master"] + } + + // Mock the nebula method + gauntlet.metaClass.nebula = { String cmd -> + if (cmd.contains('dutip')) { + return '192.168.1.100' + } + return 'nebula output' + } + + // Mock the createMFile method + gauntlet.metaClass.createMFile = { -> + // Do nothing, writeFile is already mocked + } + + // Mock the parseForLogging method + gauntlet.metaClass.parseForLogging = { String stage, String xmlFile, String boardName -> + // Do nothing for testing + } + + // Mock the logger + def mockLogger = Mock(sdg.Logger) + gauntlet.logger = mockLogger + + gauntlet.construct("NA","NA","NA","v0.31","NA") + def matlabTests = new MATLABTests() + + when: + matlabTests.stageSteps(gauntlet, board) + + then: + // Verify that the matlab command with proper arguments was called and returns success (0) + 1 * steps.sh([script: { String cmd -> + cmd.contains('sudo -u user IIO_URI="ip:192.168.1.100"') && + cmd.contains('board="' + board + '"') && + cmd.contains('M2K_URI="serial:/dev/ttyACM0,115200"') && + cmd.contains('elasticserver=localhost:9200') && + cmd.contains('timeout -s KILL 10m') && + cmd.contains('/usr/local/MATLAB/R2021a/bin/matlab') && + cmd.contains('-r "run(\'matlab_commands.m\');exit"') + }, returnStatus: true]) >> 0 // Return success status code + + // No unstable call should happen for successful test (status code 0) + 0 * steps.unstable(_) + } + + def "test handleTestResult method - status code 1"() { + given: + def steps = Mock(sdg.IStepExecutor) + def matlabTests = new MATLABTests() + + when: + matlabTests.handleTestResult(steps, 1) + + then: + 1 * steps.unstable("MATLAB: Error encountered when running the tests.") + } + + def "test handleTestResult method - status code 2"() { + given: + def steps = Mock(sdg.IStepExecutor) + def matlabTests = new MATLABTests() + + when: + matlabTests.handleTestResult(steps, 2) + + then: + 1 * steps.unstable("MATLAB: Some tests failed.") + } + + def "test handleTestResult method - status code 3"() { + given: + def steps = Mock(sdg.IStepExecutor) + def matlabTests = new MATLABTests() + + when: + matlabTests.handleTestResult(steps, 3) + + then: + 1 * steps.unstable("MATLAB: Some tests did not run to completion.") + } + + def "test handleTestResult method - status code 0"() { + given: + def steps = Mock(sdg.IStepExecutor) + def matlabTests = new MATLABTests() + + when: + matlabTests.handleTestResult(steps, 0) + + then: + 0 * steps.unstable(_) // No unstable call should happen for status code 0 + } + +} \ No newline at end of file diff --git a/test/sdg/stages/TestPowerCycleBoard.groovy b/test/sdg/stages/TestPowerCycleBoard.groovy new file mode 100644 index 00000000..9cf3c832 --- /dev/null +++ b/test/sdg/stages/TestPowerCycleBoard.groovy @@ -0,0 +1,75 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestPowerCycleBoard extends Specification { + + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class) + context = Mock(IContext.class) + } + + def "test getStageName"() { + given: + def PowerCycleBoard = new PowerCycleBoard() + + expect: + PowerCycleBoard.getStageName() == "PowerCycleBoard" + } + + def "test getCls"() { + given: + def PowerCycleBoard = new PowerCycleBoard() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + when: + def closure = PowerCycleBoard.getCls() + + then: + closure instanceof Closure + } + + def "test stageSteps - power cycle board"() { + given: + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","NA","v0.31","NA") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","NA") + + def PowerCycleBoard = new PowerCycleBoard() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + + when: + PowerCycleBoard.stageSteps(gauntlet, board) + + then: + 1 * steps.sh(['script':'nebula update-config pdu-config pdu_type --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', 'returnStdout':true]) + 1 * steps.sh(['script':'nebula update-config pdu-config outlet --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', 'returnStdout':true]) + 1 * steps.sh(['script':'nebula pdu.power-cycle -b zynq-zc702-adv7511-ad9361-fmcomms2-3 -p -o ', 'returnStdout':true]) + 1 * steps.echo('[INFO] Running PowerCycleBoard for zynq-zc702-adv7511-ad9361-fmcomms2-3') + } +} \ No newline at end of file diff --git a/test/sdg/stages/TestPyADITests.groovy b/test/sdg/stages/TestPyADITests.groovy new file mode 100644 index 00000000..8f846a75 --- /dev/null +++ b/test/sdg/stages/TestPyADITests.groovy @@ -0,0 +1,171 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestPyADITests extends Specification { + + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class) + context = Mock(IContext.class) + } + + def "test getStageName"() { + given: + def pyADITests = new PyADITests() + + expect: + pyADITests.getStageName() == "PyADITests" + } + + def "test getCls"() { + given: + def pyADITests = new PyADITests() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + when: + def closure = pyADITests.getCls() + + then: + closure instanceof Closure + } + + def "test PyADITests"() { + given: + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + // Create gauntEnv with required properties + def mockGauntEnv = getGauntEnv.call("NA","NA","NA","v0.31","NA") + mockGauntEnv.pyadi_iio_branch = "main" + mockGauntEnv.pyadi_iio_repo = "https://github.com/analogdevicesinc/pyadi-iio.git" + + steps.getGauntEnv(_,_,_,_,_) >> mockGauntEnv + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.fileExists('testhtml/report.html') >> true + steps.fileExists('testxml/zynq_zc702_adv7511_ad9361_fmcomms2_3_reports.xml') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + steps.retry(_, _) >> { int count, Closure cls -> cls.call() } + steps.checkout(_) >> null + + // Mock dir operations with closure execution + steps.dir(_, _) >> { String dirName, Closure closure -> + closure.call() + } + + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","v0.31","NA") + def pyADITests = new PyADITests() + + when: + pyADITests.stageSteps(gauntlet, board) + + then: + 1 * steps.sh('python3 -c "import ad9361"') + 1 * steps.sh(['script':'nebula update-config network-config dutip --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', 'returnStdout':true]) + 1 * steps.sh(['script':'python3 -m pytest --html=testhtml/report.html --junitxml=testxml/zynq_zc702_adv7511_ad9361_fmcomms2_3_reports.xml --adi-hw-map -v -k \'not stress and not prod\' -s --uri=ip: -m zynq_zc702_adv7511_ad9361_fmcomms2_3 --scan-verbose --capture=tee-sys', 'returnStatus':true]) + 1 * steps.junit(['testResults':'pyadi-iio/testxml/*.xml', 'allowEmptyResults':true]) + } + + def "test PyADITests - no libad9361"() { + given: + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + // Create gauntEnv with required properties + def mockGauntEnv = getGauntEnv.call("NA","NA","NA","v0.31","NA") + mockGauntEnv.pyadi_iio_branch = "main" + mockGauntEnv.pyadi_iio_repo = "https://github.com/analogdevicesinc/pyadi-iio.git" + + steps.getGauntEnv(_,_,_,_,_) >> mockGauntEnv + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.fileExists('testhtml/report.html') >> true + steps.fileExists('testxml/zynq_zc702_adv7511_ad9361_fmcomms2_3_reports.xml') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + steps.retry(_, _) >> { int count, Closure cls -> cls.call() } + steps.checkout(_) >> null + steps.sh('python3 -c "import ad9361"') >> false + + // Mock dir operations with closure execution + steps.dir(_, _) >> { String dirName, Closure closure -> + closure.call() + } + + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","v0.31","NA") + def pyADITests = new PyADITests() + + when: + pyADITests.stageSteps(gauntlet, board) + + then: + 1 * steps.sh('sudo cmake -DPYTHON_BINDINGS=ON ..') + 1 * steps.sh(['script':'python3 -m pytest --html=testhtml/report.html --junitxml=testxml/zynq_zc702_adv7511_ad9361_fmcomms2_3_reports.xml --adi-hw-map -v -k \'not stress and not prod\' -s --uri=ip: -m zynq_zc702_adv7511_ad9361_fmcomms2_3 --scan-verbose --capture=tee-sys', 'returnStatus':true]) + } + + def "test PyADITests - serial uri"() { + given: + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + // Create gauntEnv with required properties + def mockGauntEnv = getGauntEnv.call("NA","NA","NA","v0.31","NA") + mockGauntEnv.pyadi_iio_branch = "main" + mockGauntEnv.pyadi_iio_repo = "https://github.com/analogdevicesinc/pyadi-iio.git" + mockGauntEnv.iio_uri_source = "serial" + + steps.getGauntEnv(_,_,_,_,_) >> mockGauntEnv + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.fileExists('testhtml/report.html') >> true + steps.fileExists('testxml/zynq_zc702_adv7511_ad9361_fmcomms2_3_reports.xml') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + steps.retry(_, _) >> { int count, Closure cls -> cls.call() } + steps.checkout(_) >> null + steps.sh('python3 -c "import ad9361"') >> false + + // Mock dir operations with closure execution + steps.dir(_, _) >> { String dirName, Closure closure -> + closure.call() + } + + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","v0.31","NA") + def pyADITests = new PyADITests() + + when: + pyADITests.stageSteps(gauntlet, board) + + then: + 1 * steps.sh(['script':'nebula update-config uart-config baudrate --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', 'returnStdout':true]) + 1 * steps.sh(['script':'python3 -m pytest --html=testhtml/report.html --junitxml=testxml/zynq_zc702_adv7511_ad9361_fmcomms2_3_reports.xml --adi-hw-map -v -k \'not stress and not prod\' -s --uri=serial:, -m zynq_zc702_adv7511_ad9361_fmcomms2_3 --scan-verbose --capture=tee-sys', 'returnStatus':true]) + } +} \ No newline at end of file diff --git a/test/sdg/stages/TestRecoverBoard.groovy b/test/sdg/stages/TestRecoverBoard.groovy new file mode 100644 index 00000000..8361c558 --- /dev/null +++ b/test/sdg/stages/TestRecoverBoard.groovy @@ -0,0 +1,163 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestRecoverBoard extends Specification { + + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class) + context = Mock(IContext.class) + } + + def "test getStageName"() { + given: + RecoverBoard ubf_stage = new RecoverBoard() + + expect: + ubf_stage.getStageName() == "RecoverBoard" + } + + def "test getCls"() { + given: + RecoverBoard _stage = new RecoverBoard() + String board = "pluto" + + when: + def closure = _stage.getCls() + + then: + closure instanceof Closure + } + + def "test stageSteps for pluto"() { + given: + + //Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","NA","v0.31","NA") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","NA") + + RecoverBoard _stage = new RecoverBoard() + String board = "pluto" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + + when: + _stage.stageSteps(gauntlet, board) + + + then: + 1 * steps.echo('[INFO] Running RecoverBoard for ' + board) + 1 * steps.echo('[WARNING] Recover stage does not support pluto yet!') + } + + def "test stageSteps for non-pluto"() { + given: + + //Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","NA","NA","artifactory") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","artifactory") + + RecoverBoard _stage = new RecoverBoard() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + + // trigger an exception + steps.sh([ + script: 'nebula net.check-board-booted --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', + returnStdout: true + ]) >> { throw new Exception() } + + when: + _stage.stageSteps(gauntlet, board) + + + then: + 1 * steps.echo('[INFO] Running RecoverBoard for ' + board) + 1 * steps.echo('[INFO] Fetching reference boot files') + 1 * steps.sh( + 'set -o pipefail; nebula show-log dl.bootfiles --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3 ' + + '--source-root="/var/lib/tftpboot" --source=artifactory --branch="release" --filetype="boot_partition" ' + + '2>&1 | tee out.out' + ) + 1 * steps.echo('[INFO] Extracting reference fsbl and u-boot') + 1 * steps.echo('[INFO] Executing board recovery...') + 1 * steps.sh([ + script: 'nebula manager.recovery-device-manager --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3 ' + + '--folder=outs --sdcard', + returnStdout: true + ]) + } + + def "test stageSteps for non-pluto with exception"() { + given: + + //Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","NA","NA","artifactory") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","artifactory") + + RecoverBoard _stage = new RecoverBoard() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + gauntlet.set_env("env", [JOB_NAME: "test", BUILD_NUMBER: "1"]) + + // trigger an exception + steps.sh([ + script: 'nebula net.check-board-booted --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3', + returnStdout: true + ]) >> { throw new Exception() } + steps.sh('cp outs/bootgen_sysfiles.tgz .') >> { throw new Exception() } + + + when: + _stage.stageSteps(gauntlet, board) + + + then: + 1 * steps.sh(['script':'nebula netbox.disable-board --netbox-ip= --netbox-token= --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3 --failure --reason="Disabled by test 1" --power-off', 'returnStdout':true]) + thrown Exception + } + +} diff --git a/test/sdg/stages/TestSendResults.groovy b/test/sdg/stages/TestSendResults.groovy new file mode 100644 index 00000000..0ee52e41 --- /dev/null +++ b/test/sdg/stages/TestSendResults.groovy @@ -0,0 +1,76 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestSendResults extends Specification { + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class) + context = Mock(IContext.class) + } + + def "test getStageName"() { + given: + def SendResults = new SendResults() + + expect: + SendResults.getStageName() == "SendResults" + } + + def "test getCls"() { + given: + def SendResults = new SendResults() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + + when: + def closure = SendResults.getCls() + + then: + closure instanceof Closure + } + + def "test stageSteps - send results"() { + given: + // Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","NA","v0.31","NA") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","NA") + + def SendResults = new SendResults() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + steps.getEnv() >> [BUILD_NUMBER: '456', JENKINS_HOME: '/mocked/path'] + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + + when: + SendResults.stageSteps(gauntlet, board) + + then: + 1 * steps.echo("[INFO] Starting send log to elastic search") + 1 * steps.sh(['script':'telemetry log-boot-logs boot_folder_name zynq-zc702-adv7511-ad9361-fmcomms2-3 hdl_hash \'NA\' linux_hash \'NA\' boot_partition_hash \'null\' hdl_branch NA linux_branch NA boot_partition_branch NA is_hdl_release False is_linux_release False is_boot_partition_release False uboot_reached False linux_prompt_reached False drivers_enumerated 0 drivers_missing 0 dmesg_warnings_found 0 dmesg_errors_found 0 jenkins_build_number 456 jenkins_project_name \'null\' jenkins_agent null jenkins_trigger manual pytest_errors 0 pytest_failures 0 pytest_skipped 0 pytest_tests 0 matlab_errors 0 matlab_failures 0 matlab_skipped 0 matlab_tests 0 last_failing_stage NA last_failing_stage_failure NA', 'returnStdout':true]) + + } +} + + \ No newline at end of file diff --git a/test/sdg/stages/TestSimplyPrint.groovy b/test/sdg/stages/TestSimplyPrint.groovy new file mode 100644 index 00000000..415d16d9 --- /dev/null +++ b/test/sdg/stages/TestSimplyPrint.groovy @@ -0,0 +1,75 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestSimplyPrint extends Specification { + + def shell + def getGauntEnv + def gauntlet + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class); + context = Mock(IContext.class); + context.getStepExecutor() >> steps + context.isDefault() >> false + context.getStepExecutor().getGauntEnv(_,_,_,_,_) >> getGauntEnv.call(_,_,_,_,_) + ContextRegistry.registerContext(context) + + gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","NA") + } + + def "test getStageName"() { + given: + SimplyPrint simplyPrint = new SimplyPrint() + + expect: + simplyPrint.getStageName() == "SimplyPrint" + } + + def "test stageSteps"() { + given: + SimplyPrint simplyPrint = new SimplyPrint() + String board = "pluto" + + when: + simplyPrint.stageSteps(gauntlet, board) + + then: + 0 * steps.echo('[INFO] Running from SimplyPrint for pluto') + 1 * steps.echo('[WARNING] Running from SimplyPrint for pluto') + 1 * steps.echo('[ERROR] Running from SimplyPrint for pluto') + + expect: + gauntlet.get_env("debug_level") == 2 + } + + def "test getCls"() { + given: + SimplyPrint simplyPrint = new SimplyPrint() + String board = "pluto" + def closure = simplyPrint.getCls() + + when: + closure.call(gauntlet, board) + + then: + 1 * steps.echo('[INFO] Running from SimplyPrint for pluto') + 1 * steps.stage('SimplyPrint', _) + + expect: + gauntlet.get_env("debug_level") == 3 + } +} \ No newline at end of file diff --git a/test/sdg/stages/TestUpdateBOOTFiles.groovy b/test/sdg/stages/TestUpdateBOOTFiles.groovy new file mode 100644 index 00000000..9a10467b --- /dev/null +++ b/test/sdg/stages/TestUpdateBOOTFiles.groovy @@ -0,0 +1,136 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestUpdateBOOTFiles extends Specification { + + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class); + context = Mock(IContext.class); + } + + def "test getStageName"() { + given: + UpdateBOOTFiles ubf_stage = new UpdateBOOTFiles() + + expect: + ubf_stage.getStageName() == "UpdateBOOTFiles" + } + + def "test getCls"() { + given: + UpdateBOOTFiles ubf_stage = new UpdateBOOTFiles() + String board = "pluto" + def closure = ubf_stage.getCls() + + when: + closure = ubf_stage.getCls() + + then: + closure instanceof Closure + } + + def "test stageSteps for pluto"() { + given: + + //Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + context.getStepExecutor().getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","NA","v0.31","NA") + context.getStepExecutor().isUnix() >> true + context.getStepExecutor().sh(script: 'uname', returnStdout: true) >> 'Linux' + context.getStepExecutor().fileExists('out.out') >> true + context.getStepExecutor().readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","NA") + + UpdateBOOTFiles ubf_stage = new UpdateBOOTFiles() + String board = "pluto" + String ml_bootbin_case = "NA" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + + when: + ubf_stage.stageSteps(gauntlet, board, ml_bootbin_case) + + then: + 1 * steps.echo('[INFO] Running UpdateBOOTFiles for '+ board) + 1 * steps.echo('[INFO] Board name passed: ' + board) + 1 * steps.echo("[INFO] Branch: " + gauntlet.get_env("branches").toString()) + 1 * steps.sh('set -o pipefail; nebula show-log dl.bootfiles --board-name=pluto --source="github" --branch="v0.31" --filetype="firmware" 2>&1 | tee out.out') + 1 * steps.sh('set -o pipefail; nebula show-log manager.update-boot-files --board-name=pluto --folder=outs 2>&1 | tee out.out') + 1 * steps.retry(2,_) + 1 * steps.retry(1,_) + 1 * steps.echo('[INFO] Skipping checksum verification.') + 1 * steps.archiveArtifacts(artifacts: 'uart_boot_*.log', followSymlinks: false, allowEmptyArchive: true) + + assert gauntlet.get_env("elastic_logs")[board]["uboot_reached"] == "True" + assert gauntlet.get_env("elastic_logs")[board]["kernel_started"] == "True" + assert gauntlet.get_env("elastic_logs")[board]["linux_prompt_reached"] == "True" + assert gauntlet.get_env("elastic_logs")[board]["post_boot_failure"] == "False" + } + + def "test stageSteps for non-pluto"() { + + given: + //Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + context.getStepExecutor().getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("NA","NA","release","NA","artifactory") + context.getStepExecutor().isUnix() >> true + context.getStepExecutor().sh(script: 'uname', returnStdout: true) >> 'Linux' + context.getStepExecutor().fileExists('out.out') >> true + context.getStepExecutor().readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("NA","NA","NA","NA","NA") + + UpdateBOOTFiles ubf_stage = new UpdateBOOTFiles() + String board = "zynq-zc702-adv7511-ad9361-fmcomms2-3" + String ml_bootbin_case = "NA" + gauntlet.set_env("docker_args", []) + gauntlet.set_env("debug_level", 3) + def dl_cmd = 'dl.bootfiles --board-name=' + board + + ' --source-root=/var/lib/tftpboot' + + ' --source=artifactory' + + ' --branch=release' + + ' --filetype="boot_partition"' + + when: + ubf_stage.stageSteps(gauntlet, board, ml_bootbin_case) + + then: + 1 * steps.echo('[INFO] Running UpdateBOOTFiles for '+ board) + 1 * steps.echo('[INFO] Board name passed: ' + board) + 1 * steps.echo("[INFO] Branch: " + gauntlet.get_env("branches").toString()) + 1 * steps.sh('set -o pipefail; nebula show-log '+ dl_cmd + ' 2>&1 | tee out.out') + 1 * steps.sh('set -o pipefail; nebula show-log manager.update-boot-files --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3 --folder=outs 2>&1 | tee out.out') + 1 * steps.sh('set -o pipefail; nebula show-log manager.verify-checksum --board-name=zynq-zc702-adv7511-ad9361-fmcomms2-3 --folder=outs 2>&1 | tee out.out') + 1 * steps.archiveArtifacts(artifacts: 'uart_boot_*.log', followSymlinks: false, allowEmptyArchive: true) + + assert gauntlet.get_env("elastic_logs")[board]["uboot_reached"] == "True" + assert gauntlet.get_env("elastic_logs")[board]["kernel_started"] == "True" + assert gauntlet.get_env("elastic_logs")[board]["linux_prompt_reached"] == "True" + assert gauntlet.get_env("elastic_logs")[board]["post_boot_failure"] == "False" + } +} + + + + diff --git a/test/sdg/stages/TestnoOSTest.groovy b/test/sdg/stages/TestnoOSTest.groovy new file mode 100644 index 00000000..1ba70504 --- /dev/null +++ b/test/sdg/stages/TestnoOSTest.groovy @@ -0,0 +1,170 @@ +package sdg.stages + +import spock.lang.Specification +import sdg.Gauntlet +import sdg.Logger +import sdg.IStepExecutor +import sdg.ioc.* +import groovy.lang.GroovyShell + +class TestnoOStest extends Specification { + + def shell + def getGauntEnv + + IStepExecutor steps + IContext context + + def setup() { + // mock the context + shell = new GroovyShell() + getGauntEnv = shell.parse(new File('vars/getGauntEnv.groovy')) + + steps = Mock(IStepExecutor.class) + context = Mock(IContext.class) + } + + def "test getStageName"() { + given: + def noOSTest = new noOSTest() + + expect: + noOSTest.getStageName() == "noOSTest" + } + + def "test getCls"() { + given: + def noOSTest = new noOSTest() + String board = "max78000_adxl355" + + when: + def closure = noOSTest.getCls() + + then: + closure instanceof Closure + } + + def "test DownloadBinaries - non-Xilinx boards"() { + given: + //Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("main","main","NA","v0.31","NA") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("main","main","NA","NA","NA") + + def noOSTest = new noOSTest() + def board = "max78000_adxl355-vdummy_example" + + steps.sh(script: "ls outs", returnStdout: true) >> "eval-adxl355-pmdz_maxim_dummy_example_max78000_adxl355.elf" + steps.sh(script: 'nebula update-config board-config example --board-name=max78000_adxl355-vdummy_example', returnStdout: true) >> "dummy_example" + steps.sh(script: 'nebula update-config downloader-config platform --board-name=max78000_adxl355-vdummy_example', returnStdout: true) >> "maxim" + + when: + def filepath = noOSTest.DownloadBinaries(gauntlet, board) + + then: + 1 * steps.sh('set -o pipefail; nebula show-log dl.bootfiles --board-name=max78000_adxl355-vdummy_example --source-root="/var/lib/tftpboot" --source=NA --branch="main" --filetype="noos" 2>&1 | tee out.out') + assert filepath == "outs/eval-adxl355-pmdz_maxim_dummy_example_max78000_adxl355.elf" + } + + def "test DownloadBinaries -no file found"() { + given: + //Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("main","main","NA","v0.31","NA") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("main","main","NA","NA","NA") + + def noOSTest = new noOSTest() + def board = "max78000_adxl355-vdummy_example" + + steps.sh(script: "ls outs", returnStdout: true) >> "eval-adxl355-pmdz_maxim_dummy_example.elf" + steps.sh(script: 'nebula update-config board-config example --board-name=max78000_adxl355-vdummy_example', returnStdout: true) >> "dummy_example" + steps.sh(script: 'nebula update-config downloader-config platform --board-name=max78000_adxl355-vdummy_example', returnStdout: true) >> "maxim" + + when: + def filepath = noOSTest.DownloadBinaries(gauntlet, board) + + then: + 1 * steps.sh('set -o pipefail; nebula show-log dl.bootfiles --board-name=max78000_adxl355-vdummy_example --source-root="/var/lib/tftpboot" --source=NA --branch="main" --filetype="noos" 2>&1 | tee out.out') + thrown Exception + } + + def "test RunTests - non-Xilinx boards - flash failed"() { + given: + //Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("main","main","NA","v0.31","NA") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("main","main","NA","NA","NA") + + def noOSTest = new noOSTest() + def board = "max78000_adxl355-vdummy_example" + + steps.sh(script: 'nebula update-config board-config example --board-name=max78000_adxl355-vdummy_example', returnStdout: true) >> "dummy_example" + steps.sh(script: 'nebula update-config downloader-config platform --board-name=max78000_adxl355-vdummy_example', returnStdout: true) >> "maxim" + def filepath = "outs/eval-adxl355-pmdz_maxim_dummy_example_max78000_adxl355.elf" + def jtag_cable_id = "123456" + def flashStatus = 0 + + + when: + noOSTest.RunTests(gauntlet, board, filepath) + + then: + 1 * steps.sh(['returnStatus':true, 'script':'./mcufla.sh outs/eval-adxl355-pmdz_maxim_dummy_example_max78000_adxl355.elf ']) + 1 * steps.sh(['script':'nebula update-config jtag-config jtag_cable_id --board-name=max78000_adxl355-vdummy_example', 'returnStdout':true]) + thrown Exception + } + + def "test RunTests - Xilinx boards - flash successful"() { + given: + //Mock gauntlet + context.getStepExecutor() >> steps + context.isDefault() >> false + steps.getGauntEnv(_,_,_,_,_) >> getGauntEnv.call("main","main","NA","v0.31","NA") + steps.isUnix() >> true + steps.sh(script: 'uname', returnStdout: true) >> 'Linux' + steps.fileExists('out.out') >> true + steps.readFile('out.out') >> 'STDOUT of some successful nebula command' + ContextRegistry.registerContext(context) + Gauntlet gauntlet = new Gauntlet() + gauntlet.construct("main","main","NA","NA","NA") + + def noOSTest = new noOSTest() + def board = "kcu105_adrv9371x-viio" + + steps.sh(script: 'nebula update-config board-config example --board-name=kcu105_adrv9371x-viio', returnStdout: true) >> "iio" + steps.sh(script: 'nebula update-config downloader-config platform --board-name=kcu105_adrv9371x-viio', returnStdout: true) >> "Xilinx" + def filepath = "ad9371_xilinx_iio_adrv9371x_kcu105.elf" + + when: + noOSTest.RunTests(gauntlet, board, filepath) + + then: + + 1 * steps.sh(['script':'nebula update-config downloader-config no_os_project --board-name=kcu105_adrv9371x-viio', 'returnStdout':true]) + 1 * steps.sh(['script':'nebula update-config uart-config baudrate --board-name=kcu105_adrv9371x-viio', 'returnStdout':true]) + 1 * steps.archiveArtifacts(['artifacts':'*-boot.log', 'followSymlinks':false, 'allowEmptyArchive':true]) + + } +} + diff --git a/vars/dockerBuilds.groovy b/vars/dockerBuilds.groovy index 269d54f6..21b9a354 100644 --- a/vars/dockerBuilds.groovy +++ b/vars/dockerBuilds.groovy @@ -1,3 +1,4 @@ +import com.cloudbees.groovy.cps.NonCPS ///////////////////////////////////////////////////// /* diff --git a/vars/getGauntEnv.groovy b/vars/getGauntEnv.groovy index 3a6f3310..efbaa468 100644 --- a/vars/getGauntEnv.groovy +++ b/vars/getGauntEnv.groovy @@ -98,6 +98,7 @@ private def call(hdlBranch, linuxBranch, bootPartitionBranch,firmwareVersion, bo internal_stages_to_skip: [:], // Number of stages to skip. Used for test skipping for MATLAB update_lib_requirements: false, // Set to true to run installation of requirements.txt of nebula and telemetry update_container_lib: false, // Set to true to force update libiio, nebula, telemetry base on master branch inside docker container - nebula_config_path: '' + nebula_config_path: '', + debug_level: 1, ] } diff --git a/vars/getGauntlet.groovy b/vars/getGauntlet.groovy index 4530f50a..fe501302 100644 --- a/vars/getGauntlet.groovy +++ b/vars/getGauntlet.groovy @@ -1,5 +1,11 @@ +import sdg.ioc.ContextRegistry +import sdg.Gauntlet + def call(hdlBranch="NA", linuxBranch="NA", bootPartitionBranch="release",firmwareVersion="NA", bootfile_source="artifactory") { - def harness = new sdg.Gauntlet() + + ContextRegistry.registerDefaultContext(this) + def harness = new Gauntlet() harness.construct(hdlBranch, linuxBranch, bootPartitionBranch, firmwareVersion, bootfile_source) return harness + } diff --git a/vars/getStage.groovy b/vars/getStage.groovy new file mode 100644 index 00000000..0cfdf531 --- /dev/null +++ b/vars/getStage.groovy @@ -0,0 +1,9 @@ +def call(className){ + def classPath = "../src/sdg/stages" + def classLoader = new GroovyClassLoader() + classLoader.addClasspath(classPath) + + // load stage class + def stageClass = classLoader.loadClass("sdg.stages.${className}") + return stageClass.newInstance() +} \ No newline at end of file diff --git a/vars/registerContext.groovy b/vars/registerContext.groovy new file mode 100644 index 00000000..4545e2cc --- /dev/null +++ b/vars/registerContext.groovy @@ -0,0 +1,6 @@ +import sdg.IStepExecutor; +import sdg.ioc.ContextRegistry + +def call(steps) { + ContextRegistry.registerDefaultContext(steps) +} diff --git a/vars/uploadArtifactory.groovy b/vars/uploadArtifactory.groovy index 0b0be767..aa098f5b 100644 --- a/vars/uploadArtifactory.groovy +++ b/vars/uploadArtifactory.groovy @@ -1,3 +1,5 @@ +import com.cloudbees.groovy.cps.NonCPS + def call(project, filepattern) { root = 'sdg-generic-development/' ext = ''