├── .codecov.yml ├── .github └── workflows │ ├── tests.yml │ └── ui-build.yaml ├── .gitignore ├── AUTHORS ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SPAG.md ├── assets ├── PNG │ ├── ConTest_logo_black_transparent.png │ ├── ConTest_logo_black_with_bg.png │ ├── ConTest_logo_color_transparent.png │ ├── ConTest_logo_color_with_bg.png │ ├── ConTest_logo_white_transparent.png │ └── ConTest_logo_white_with_bg.png └── SVG │ ├── ConTest_logo_black_transparent.svg │ ├── ConTest_logo_black_with_bg.svg │ ├── ConTest_logo_color_transparent.svg │ ├── ConTest_logo_color_with_bg.svg │ ├── ConTest_logo_white_transparent.svg │ └── ConTest_logo_white_with_bg.svg ├── cmds ├── admin_server │ ├── .gitignore │ ├── job │ │ ├── rdb │ │ │ └── rdb.go │ │ └── storage.go │ ├── main.go │ ├── server │ │ ├── server.go │ │ └── static.go │ ├── storage │ │ ├── mongo │ │ │ └── mongo.go │ │ └── storage.go │ └── ui │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ └── index.html │ │ ├── src │ │ ├── api │ │ │ ├── logs.ts │ │ │ └── tags.ts │ │ ├── app.scss │ │ ├── app.tsx │ │ ├── date_cell │ │ │ └── date_cell.tsx │ │ ├── index.tsx │ │ ├── jobs │ │ │ └── jobs.tsx │ │ ├── search_logs │ │ │ ├── log_table │ │ │ │ ├── log_table.scss │ │ │ │ └── log_table.tsx │ │ │ ├── search_logs.scss │ │ │ └── search_logs.tsx │ │ └── search_tags │ │ │ ├── search_tags.scss │ │ │ ├── search_tags.tsx │ │ │ └── tags_grid │ │ │ ├── tags_grid.scss │ │ │ └── tags_grid.tsx │ │ ├── tsconfig.json │ │ └── webpack.config.js ├── clients │ └── contestcli │ │ ├── cli │ │ ├── cli.go │ │ └── verbs.go │ │ ├── descriptors │ │ ├── debug.json │ │ ├── fwtesting │ │ │ └── lspci.json │ │ ├── start-literal-with-cleanup.json │ │ ├── start-literal.json │ │ ├── start-literal.yaml │ │ └── start.json │ │ └── main.go ├── contest-generator │ ├── README.md │ ├── config.go │ ├── config_test.go │ ├── contest.go.template │ ├── core-plugins.yml │ └── main.go ├── contest │ ├── hosts.csv │ ├── main.go │ ├── server │ │ └── server.go │ └── test_samples │ │ ├── randecho.json │ │ ├── sleep.json │ │ ├── slowecho.json │ │ ├── sshhostname.json │ │ └── terminalexpect.json └── exec_agent │ ├── main.go │ └── verbs.go ├── db └── rdbms │ ├── migration │ ├── 0001_add_extended_descriptor.sql │ ├── 0002_migrate_descriptor_to_extended_descriptor.go │ ├── 0003_add_jobs_state_column.sql │ ├── 0004_add_job_tags_table.sql │ ├── 0005_event_payload_mediumtext.sql │ ├── 0006_add_indices.sql │ ├── 0007_add_test_attempt_column.sql │ └── README.md │ └── schema │ └── v0 │ └── create_contest_db.sql ├── docker-compose.mariadb.yml ├── docker-compose.yml ├── docker ├── contest │ ├── Dockerfile │ └── tests.sh ├── mariadb │ ├── Dockerfile │ ├── initdb.sql │ └── migration.sh ├── mongo │ ├── Dockerfile │ └── initdb.js └── mysql │ ├── Dockerfile │ ├── initdb.sql │ └── migration.sh ├── go.mod ├── go.sum ├── pkg ├── api │ ├── api.go │ ├── api_test.go │ ├── event.go │ ├── listener.go │ ├── options.go │ ├── response.go │ └── values_proxy_context.go ├── cerrors │ └── cerrors.go ├── config │ ├── db.go │ ├── jobdescriptor.go │ ├── storage.go │ └── timeouts.go ├── event │ ├── common.go │ ├── errors.go │ ├── frameworkevent │ │ └── framework.go │ ├── internal │ │ ├── querytools │ │ │ └── apply_query_field.go │ │ └── reflecttools │ │ │ └── is_zero.go │ ├── query.go │ └── testevent │ │ ├── test.go │ │ └── test_test.go ├── job │ ├── events.go │ ├── events_test.go │ ├── job.go │ ├── job_test.go │ ├── reporting.go │ ├── request.go │ ├── status.go │ └── tags.go ├── jobmanager │ ├── bundles.go │ ├── job.go │ ├── job_test.go │ ├── jobmanager.go │ ├── list.go │ ├── options.go │ ├── resume.go │ ├── retry.go │ ├── start.go │ ├── status.go │ ├── steps.go │ └── stop.go ├── lib │ └── comparison │ │ ├── comparison.go │ │ └── comparison_test.go ├── loggerhook │ └── httphook.go ├── logging │ ├── default_options.go │ └── logger.go ├── metrics │ └── perf │ │ └── perf.go ├── pluginregistry │ ├── bundles.go │ ├── errors.go │ ├── pluginregistry.go │ └── pluginregistry_test.go ├── remote │ ├── monitor.go │ ├── proto.go │ ├── proto_test.go │ ├── sync.go │ └── sync_test.go ├── runner │ ├── base_test_suite_test.go │ ├── event.go │ ├── job_runner.go │ ├── job_runner_test.go │ ├── job_status.go │ ├── step_runner.go │ ├── step_runner_test.go │ ├── step_state.go │ ├── test_runner.go │ ├── test_runner_test.go │ └── test_steps_variables.go ├── signaling │ ├── signaler.go │ ├── signaler_flaky_test.go │ └── signaler_test.go ├── signals │ └── paused.go ├── storage │ ├── events.go │ ├── events_test.go │ ├── job.go │ ├── job_query.go │ ├── job_test.go │ ├── limits │ │ ├── limits.go │ │ └── limits_test.go │ ├── storage.go │ ├── storage_test.go │ ├── vault.go │ └── vault_test.go ├── target │ ├── locker.go │ ├── target.go │ ├── target_test.go │ └── targetmanager.go ├── test │ ├── fetcher.go │ ├── functions.go │ ├── param_expander.go │ ├── param_expander_test.go │ ├── parameter.go │ ├── parameter_test.go │ ├── result.go │ ├── step.go │ ├── step_test.go │ └── test.go ├── transport │ ├── http │ │ └── http.go │ └── transport.go ├── types │ └── types.go └── userfunctions │ ├── donothing │ └── donothing.go │ └── ocp │ └── ocp.go ├── plugins ├── listeners │ └── httplistener │ │ └── httplistener.go ├── reporters │ ├── noop │ │ └── noop.go │ └── targetsuccess │ │ └── targetsuccess.go ├── storage │ ├── memory │ │ ├── memory.go │ │ └── memory_test.go │ └── rdbms │ │ ├── events.go │ │ ├── init.go │ │ ├── list_jobs.go │ │ ├── report.go │ │ └── request.go ├── targetlocker │ ├── dblocker │ │ └── dblocker.go │ ├── inmemory │ │ └── inmemory.go │ └── noop │ │ ├── noop.go │ │ └── noop_test.go ├── targetmanagers │ ├── csvtargetmanager │ │ └── csvfile.go │ └── targetlist │ │ └── targetlist.go ├── testfetchers │ ├── literal │ │ └── literal.go │ └── uri │ │ └── uri.go └── teststeps │ ├── cmd │ └── cmd.go │ ├── cpucmd │ └── cpucmd.go │ ├── echo │ └── echo.go │ ├── example │ └── example.go │ ├── exec │ ├── events.go │ ├── exec.go │ ├── ocp_parser.go │ ├── ocp_parser_test.go │ ├── readme.md │ ├── runner.go │ └── transport │ │ ├── local_transport.go │ │ ├── local_transport_safe.go │ │ ├── ssh_process.go │ │ ├── ssh_process_async.go │ │ ├── ssh_transport.go │ │ ├── transport.go │ │ └── types.go │ ├── gathercmd │ └── gathercmd.go │ ├── qemu │ ├── README.md │ └── qemu.go │ ├── randecho │ └── randecho.go │ ├── s3fileupload │ └── s3fileupload.go │ ├── sleep │ └── sleep.go │ ├── sshcmd │ └── sshcmd.go │ ├── terminalexpect │ └── terminalexpect.go │ ├── teststeps.go │ ├── teststeps_test.go │ ├── variables │ ├── readme.md │ ├── variables.go │ └── variables_test.go │ └── waitport │ ├── waitport.go │ └── waitport_test.go ├── run_lint.sh ├── run_tests.sh ├── tests ├── common │ ├── get_events.go │ └── goroutine_leak_check │ │ ├── goroutine_leak_check.go │ │ └── goroutine_leak_check_test.go ├── e2e │ ├── e2e_test.go │ ├── test-resume.yaml │ ├── test-retry.yaml │ ├── test-simple.yaml │ └── test-variables.yaml ├── integ │ ├── admin_server │ │ ├── flags_test.go │ │ ├── getlogs_test.go │ │ ├── group_jobs_by_project_test.go │ │ └── logendpoint_test.go │ ├── common │ │ └── storage.go │ ├── events │ │ ├── frameworkevents │ │ │ ├── common.go │ │ │ ├── frameworkevents_memory_test.go │ │ │ └── frameworkevents_rdbms_test.go │ │ └── testevents │ │ │ ├── common.go │ │ │ ├── testevents_memory_test.go │ │ │ └── testevents_rdbms_test.go │ ├── job │ │ ├── common.go │ │ ├── job_memory_test.go │ │ └── job_rdbms_test.go │ ├── jobmanager │ │ ├── common.go │ │ ├── common_longtest.go │ │ ├── jobdescriptors.go │ │ ├── jobmanager_memory_test.go │ │ └── jobmanager_rdbms_test.go │ ├── migration │ │ ├── 0002_migrate_descriptor_to_extended_descriptor_test.go │ │ └── common.go │ └── plugins │ │ ├── cmd_plugin_test.go │ │ ├── exec_plugin_test.go │ │ └── testrunner_test.go └── plugins │ ├── reporters │ └── readmeta │ │ └── readmeta.go │ ├── targetlist_with_state │ └── targetlist_with_state.go │ ├── targetlocker │ ├── targetlocker_common_test.go │ ├── targetlocker_dblocker_test.go │ └── targetlocker_inmemory_test.go │ ├── targetmanagers │ └── readmeta │ │ └── readmeta.go │ └── teststeps │ ├── badtargets │ └── badtargets.go │ ├── channels │ └── channels.go │ ├── crash │ └── crash.go │ ├── fail │ └── fail.go │ ├── hanging │ └── hanging.go │ ├── noop │ └── noop.go │ ├── noreturn │ └── noreturn.go │ ├── panicstep │ └── panicstep.go │ ├── readmeta │ └── readmeta.go │ ├── slowecho │ └── slowecho.go │ └── teststep │ └── teststep.go └── tools ├── checklicenses-config.json ├── deps.go └── migration └── rdbms ├── README.md ├── main.go ├── migrate └── migrate.go └── migrationlib └── version.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: 4 | default: 5 | target: 30% 6 | project: 7 | default: 8 | target: auto 9 | threshold: 20% 10 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-22.04 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-go@v2 11 | with: 12 | stable: "false" 13 | go-version: "1.18" 14 | - name: Run Linters 15 | run: ./run_lint.sh 16 | - name: Run Tests 17 | run: CI=true ./run_tests.sh 18 | contest-generator: 19 | runs-on: ubuntu-22.04 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | # clone in the gopath 24 | path: src/github.com/${{ github.repository }} 25 | - uses: actions/setup-go@v2 26 | with: 27 | stable: "false" 28 | go-version: "1.18" 29 | - name: set up environment variables 30 | run: | 31 | # must do this here because `env` doesn't allow variable expansion 32 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV 33 | echo "GOBIN=$GITHUB_WORKSPACE/bin" >> $GITHUB_ENV 34 | - name: Validate contest-generator 35 | run: | 36 | set -exu 37 | cd "${GITHUB_WORKSPACE}"/src/github.com/${{ github.repository }}/cmds/contest-generator 38 | go build 39 | builddir=$(./contest-generator --from core-plugins.yml) # generate the code and get the output directory 40 | cd "${builddir}" 41 | ls -l 42 | go mod init contest 43 | go mod edit -replace "github.com/${{ github.repository}}=${GITHUB_WORKSPACE}/src/github.com/${{ github.repository}}" # ensure we are building against the code from this commit 44 | go mod tidy 45 | go build # build the generated main.go 46 | gofmt -w "${builddir}/contest.go" # ensure that the code respects Go's format guidelines 47 | diff -Naur "${builddir}/contest.go" "${GITHUB_WORKSPACE}"/src/github.com/${{ github.repository }}/cmds/contest/main.go # show the differences between the newly generated code and the existing one 48 | -------------------------------------------------------------------------------- /.github/workflows/ui-build.yaml: -------------------------------------------------------------------------------- 1 | name: Web UI Build 2 | on: 3 | push: 4 | paths: 5 | - 'cmds/admin_server/ui/**' 6 | pull_request: 7 | paths: 8 | - 'cmds/admin_server/ui/**' 9 | jobs: 10 | build: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-go@v2 15 | with: 16 | stable: "false" 17 | go-version: "1.18" 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: "16.15.1" 21 | - name: Build npm ui 22 | run: | 23 | cd ./cmds/admin_server/ui 24 | npm install 25 | npm run build 26 | - name: Embed web/ui into go file 27 | run: | 28 | set -exu 29 | cd ./cmds/admin_server/ui/dist 30 | go get -u github.com/mjibson/esc 31 | go install github.com/mjibson/esc 32 | export PATH=$PATH:$(go env GOPATH)/bin 33 | # setting modtime to a dummy fixed value to avoid unnecessary commits 34 | esc -o ../../server/static.go -pkg=server -modtime=0 -prefix=/ ./ 35 | - uses: EndBug/add-and-commit@v9 36 | with: 37 | message: 'embed web/ui into go file' 38 | default_author: github_actions 39 | add: '-f ./cmds/admin_server/server/static.go' 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmds/contest/contest 2 | cmds/clients/contestcli/contestcli 3 | cmds/contest-generator/contest-generator 4 | tools/migration/rdbms/rdbms 5 | .*.swp 6 | profile.out 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Andrea Barberio 2 | Marco Guerri 3 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax 2 | 3 | * @linuxboot/team-contest 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ConTest 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Pull Requests 6 | We actively welcome your pull requests. 7 | 8 | 1. Fork the repo and create your branch from `master`. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. If you've changed APIs, update the documentation. 11 | 4. Ensure the test suite passes. 12 | 5. Make sure your code lints. 13 | 6. We request a DCO, so be sure to git commit -s 14 | 15 | ## Issues 16 | We use GitHub issues to track public bugs. Please ensure your description is 17 | clear and has sufficient instructions to be able to reproduce the issue. 18 | 19 | ## Coding Style 20 | We strive to follow the official Go coding style, see https://github.com/golang/go/wiki/CodeReviewComments . 21 | 22 | ## License 23 | By contributing to ConTest, you agree that your contributions will be licensed 24 | under the LICENSE file in the root directory of this source tree. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/PNG/ConTest_logo_black_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxboot/contest/d1dd884008f956b9cefb3a9ae53d0b0139c74cdf/assets/PNG/ConTest_logo_black_transparent.png -------------------------------------------------------------------------------- /assets/PNG/ConTest_logo_black_with_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxboot/contest/d1dd884008f956b9cefb3a9ae53d0b0139c74cdf/assets/PNG/ConTest_logo_black_with_bg.png -------------------------------------------------------------------------------- /assets/PNG/ConTest_logo_color_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxboot/contest/d1dd884008f956b9cefb3a9ae53d0b0139c74cdf/assets/PNG/ConTest_logo_color_transparent.png -------------------------------------------------------------------------------- /assets/PNG/ConTest_logo_color_with_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxboot/contest/d1dd884008f956b9cefb3a9ae53d0b0139c74cdf/assets/PNG/ConTest_logo_color_with_bg.png -------------------------------------------------------------------------------- /assets/PNG/ConTest_logo_white_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxboot/contest/d1dd884008f956b9cefb3a9ae53d0b0139c74cdf/assets/PNG/ConTest_logo_white_transparent.png -------------------------------------------------------------------------------- /assets/PNG/ConTest_logo_white_with_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxboot/contest/d1dd884008f956b9cefb3a9ae53d0b0139c74cdf/assets/PNG/ConTest_logo_white_with_bg.png -------------------------------------------------------------------------------- /cmds/admin_server/.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/dist/ -------------------------------------------------------------------------------- /cmds/admin_server/job/storage.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/linuxboot/contest/pkg/types" 8 | ) 9 | 10 | // DB wraps a job database 11 | type Storage interface { 12 | GetTags(ctx context.Context, tagPattern string) ([]Tag, error) 13 | GetJobs(ctx context.Context, projectName string) ([]Job, error) 14 | } 15 | 16 | // Tag contains metadata about jobs under a given tag 17 | type Tag struct { 18 | Name string 19 | // number of jobs with under this tag 20 | JobsCount uint 21 | } 22 | 23 | // Job contains final report data about that job_id 24 | type Job struct { 25 | JobID types.JobID 26 | // fields for the final report of the job if it exists. 27 | ReporterName *string 28 | ReportTime *time.Time 29 | Success *bool 30 | Data *string 31 | } 32 | -------------------------------------------------------------------------------- /cmds/admin_server/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | var ( 10 | ErrReadOnlyStorage = errors.New("error read only storage") 11 | ErrInsert = errors.New("error inserting into the db") 12 | ErrConstructQuery = errors.New("error forming db query from api query") 13 | ErrQuery = errors.New("error querying from the database") 14 | ) 15 | 16 | var ( 17 | DefaultTimestampFormat = "2006-01-02T15:04:05.000Z07:00" 18 | ) 19 | 20 | type Storage interface { 21 | StoreLogs(ctx context.Context, logs []Log) error 22 | GetLogs(ctx context.Context, query Query) (*Result, error) 23 | } 24 | 25 | // Log defines the basic log info pushed by the server 26 | type Log struct { 27 | JobID uint64 28 | LogData string 29 | Date time.Time 30 | LogLevel string 31 | } 32 | 33 | // Query defines the different options to filter with 34 | type Query struct { 35 | JobID *uint64 36 | Text *string 37 | LogLevel *string 38 | StartDate *time.Time 39 | EndDate *time.Time 40 | PageSize uint 41 | Page uint 42 | } 43 | 44 | // Result defines the expected result returned from the db 45 | type Result struct { 46 | Logs []Log 47 | Count uint64 48 | Page uint 49 | PageSize uint 50 | } 51 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "serve": "webpack serve --mode development", 8 | "build": "webpack --mode production" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@babel/core": "^7.18.9", 15 | "@babel/preset-env": "^7.18.9", 16 | "@babel/preset-react": "^7.18.6", 17 | "@babel/preset-typescript": "^7.18.6", 18 | "@types/react": "^18.0.15", 19 | "@types/react-dom": "^18.0.6", 20 | "@types/superagent": "^4.1.15", 21 | "babel-loader": "^8.2.5", 22 | "css-loader": "^6.7.1", 23 | "html-webpack-plugin": "^5.5.0", 24 | "prettier": "^2.7.1", 25 | "sass": "^1.53.0", 26 | "sass-loader": "^13.0.2", 27 | "style-loader": "^3.3.1", 28 | "typescript": "^4.7.4", 29 | "webpack": "^5.73.0", 30 | "webpack-cli": "^4.10.0", 31 | "webpack-dev-server": "^4.9.3" 32 | }, 33 | "dependencies": { 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-router-dom": "^6.3.0", 37 | "rsuite": "^5.16.1", 38 | "superagent": "^8.0.0" 39 | }, 40 | "prettier": { 41 | "singleQuote": true, 42 | "trailingComma": "es5", 43 | "tabWidth": 4, 44 | "useTabs": false 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/api/logs.ts: -------------------------------------------------------------------------------- 1 | import superagent from 'superagent'; 2 | 3 | // TODO: remove the hardcoded levels 4 | // logLevels is the possible levels for logs 5 | export const Levels = ['panic', 'fatal', 'error', 'warning', 'info', 'debug']; 6 | 7 | // Log defines the expected log entry returned from the api 8 | export interface Log { 9 | job_id: number; 10 | log_data: string; 11 | log_level: string; 12 | date: string; 13 | } 14 | 15 | // Query defines all the possible filters to form a query 16 | export interface Query { 17 | job_id?: number; 18 | text?: string; 19 | log_level?: string; 20 | start_date?: string; 21 | end_date?: string; 22 | page: number; 23 | page_size: number; 24 | } 25 | 26 | // Result defines the structure of the api response to a query 27 | export interface Result { 28 | logs: Log[] | null; 29 | count: number; 30 | page: number; 31 | page_size: number; 32 | } 33 | 34 | // getLogs returns Result that contains logs fetched according to the Query 35 | export async function getLogs(query: Query): Promise { 36 | let result: superagent.Response = await superagent.get('/log').query(query); 37 | 38 | return result.body; 39 | } 40 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/api/tags.ts: -------------------------------------------------------------------------------- 1 | import superagent from 'superagent'; 2 | 3 | export interface Tag { 4 | name: string; 5 | jobs_count: number; 6 | } 7 | 8 | export interface TagQuery { 9 | text: string; 10 | } 11 | 12 | export interface Report { 13 | name: string; 14 | time: string; 15 | success: boolean; 16 | data: string; 17 | } 18 | export interface Job { 19 | job_id: number; 20 | report?: Report; 21 | } 22 | 23 | export async function getTags(query: TagQuery): Promise { 24 | let result: superagent.Response = await superagent.get('/tag').query(query); 25 | 26 | return result.body; 27 | } 28 | 29 | export async function getJobs(job_name: string): Promise { 30 | let result: superagent.Response = await superagent.get( 31 | `/tag/${job_name}/jobs` 32 | ); 33 | 34 | return result.body; 35 | } 36 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/app.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | .navbar { 8 | padding: 1em 2em; 9 | display: flex; 10 | align-items: center; 11 | box-shadow: 1px 1px 10px rgba(0,0,0,.2); 12 | 13 | .navbar__logo { 14 | font-size: 1.7em; 15 | font-weight: 900; 16 | } 17 | 18 | .navbar__btns{ 19 | margin-left: auto; 20 | } 21 | 22 | .navbar__link{ 23 | padding: .5em .7em; 24 | border: 1px solid; 25 | border-radius: 5px; 26 | } 27 | 28 | .navbar__link + .navbar__link { 29 | margin-left: 10px; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Routes, Route, Link } from 'react-router-dom'; 3 | import SearchTags from './search_tags/search_tags'; 4 | import SearchLogs from './search_logs/search_logs'; 5 | import Jobs from './jobs/jobs'; 6 | import './app.scss'; 7 | 8 | export default function App() { 9 | return ( 10 | <> 11 |
12 |
ConTest
13 |
14 | 19 | Logs 20 | 21 | 26 | Tags 27 | 28 |
29 |
30 | 31 | 32 | } /> 33 | 34 | } /> 35 | } /> 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/date_cell/date_cell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Cell, CellProps } from 'rsuite-table'; 3 | 4 | function resolveDatakey(rowData: any, dataKey?: string): string | undefined { 5 | if (!rowData || !dataKey) return undefined; 6 | if (dataKey.split('.').length == 1) return rowData[dataKey]; 7 | let keys = dataKey.split('.'); 8 | return resolveDatakey(rowData[keys[0]], keys.slice(1).join('.')); 9 | } 10 | 11 | export default function DateCell({ rowData, dataKey, ...props }: CellProps) { 12 | let data = dataKey && resolveDatakey(rowData, dataKey); 13 | return ( 14 | 15 |

{data && new Date(data).toLocaleString()}

16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | import ReactDOM from 'react-dom'; 4 | import App from './app'; 5 | 6 | const el = document.getElementById('app'); 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | el 13 | ); 14 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/search_logs/log_table/log_table.scss: -------------------------------------------------------------------------------- 1 | .log-table { 2 | .log-table__search-btn { 3 | display: block !important; 4 | margin: auto; 5 | } 6 | 7 | .log-table__cell { 8 | padding: 2px; 9 | } 10 | 11 | .log-table__pagination { 12 | padding: 2px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/search_logs/search_logs.scss: -------------------------------------------------------------------------------- 1 | .logs-search { 2 | padding: 1.3em; 3 | 4 | .logs-search__input-group { 5 | display: flex; 6 | align-items: center; 7 | padding: 0.3em; 8 | 9 | p { 10 | width: 9em; 11 | } 12 | 13 | .filter-input { 14 | width: 30em; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/search_tags/search_tags.scss: -------------------------------------------------------------------------------- 1 | .tags-search { 2 | padding: 1.3em; 3 | 4 | .tags-search__input-group { 5 | display: flex; 6 | align-items: center; 7 | padding: 0.3em; 8 | 9 | p { 10 | width: 9em; 11 | } 12 | 13 | .filter-input { 14 | width: 30em; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/search_tags/search_tags.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Input, Checkbox } from 'rsuite'; 3 | import TagsGrid from './tags_grid/tags_grid'; 4 | import './search_tags.scss'; 5 | 6 | const ProjectPrefix = 'project_'; 7 | 8 | export default function SearchTags() { 9 | const [tagPattern, setTagPattern] = useState(''); 10 | const [searchProjects, setSearchProjects] = useState(false); 11 | 12 | const setPrefix = (checked: boolean) => { 13 | if (checked) { 14 | if (!tagPattern.startsWith(ProjectPrefix)) 15 | setTagPattern((value) => `${ProjectPrefix}${value}`); 16 | } 17 | }; 18 | 19 | return ( 20 |
21 |
22 |

Search:

23 | { 28 | // handles making the project prefix always in the input field in case of project search 29 | if ( 30 | searchProjects && 31 | (value === 32 | ProjectPrefix.substring( 33 | 0, 34 | ProjectPrefix.length - 1 35 | ) || 36 | value === '') 37 | ) { 38 | setTagPattern(ProjectPrefix); 39 | } else { 40 | setTagPattern(value); 41 | } 42 | }} 43 | /> 44 | { 47 | setSearchProjects(checked); 48 | setPrefix(checked); 49 | }} 50 | > 51 | Search Projects 52 | 53 |
54 | 55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/search_tags/tags_grid/tags_grid.scss: -------------------------------------------------------------------------------- 1 | .tag-grid { 2 | .tag-grid__search-btn{ 3 | display: block; 4 | margin: 2px auto; 5 | } 6 | .tags{ 7 | display: flex; 8 | flex-wrap: wrap; 9 | flex-shrink: 0; 10 | justify-items: center; 11 | align-items: center; 12 | grid-gap: .5em; 13 | padding: 2em; 14 | 15 | strong{ 16 | margin: 0 .3em; 17 | } 18 | 19 | & > div { 20 | width: 250px; 21 | padding: 1em; 22 | border: 1px solid #d3d3d3 ; 23 | border-radius: 5px; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /cmds/admin_server/ui/src/search_tags/tags_grid/tags_grid.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Button, useToaster, Message } from 'rsuite'; 4 | import { StandardProps } from 'rsuite-table/lib/@types/common'; 5 | import { TypeAttributes } from 'rsuite/esm/@types/common'; 6 | import { Tag, getTags } from '../../api/tags'; 7 | import './tags_grid.scss'; 8 | 9 | export interface TagsGridProps extends StandardProps { 10 | tagPattern: string; 11 | } 12 | 13 | export default function TagsGrid(props: TagsGridProps) { 14 | const [tags, setTags] = useState([]); 15 | const toaster = useToaster(); 16 | 17 | const showMsg = (type: TypeAttributes.Status, message: string) => { 18 | toaster.push( 19 | 20 | {message} 21 | , 22 | { placement: 'topEnd' } 23 | ); 24 | }; 25 | 26 | const updateGrid = async () => { 27 | try { 28 | let result: Tag[] = await getTags({ 29 | text: props.tagPattern, 30 | }); 31 | 32 | setTags(result || []); 33 | } catch (err) { 34 | showMsg('error', err?.message); 35 | } 36 | }; 37 | 38 | return ( 39 |
40 |
41 | 50 |
51 |
52 | {tags.map((t, key) => ( 53 |
54 |

55 | Tag Name: 56 | {t.name} 57 |

58 |

59 | Number of Jobs: 60 | {t.jobs_count} 61 |

62 |
63 | ))} 64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /cmds/admin_server/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": false, 19 | "jsx": "react-jsx" 20 | }, 21 | "include": [ 22 | "src" 23 | ] 24 | } -------------------------------------------------------------------------------- /cmds/admin_server/ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/index.tsx', 6 | output: { 7 | path: path.join(__dirname, '/dist'), 8 | filename: 'bundle.js', 9 | publicPath: '/app', 10 | }, 11 | resolve: { 12 | extensions: ['.tsx', '.ts', '.js'], 13 | }, 14 | plugins: [ 15 | new HtmlWebpackPlugin({ 16 | template: 'public/index.html', 17 | title: 'ConTest Debugging Tool', 18 | }), 19 | ], 20 | devServer: { 21 | historyApiFallback: true, 22 | port: 3030, 23 | hot: true, 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | exclude: /node_modules/, 30 | use: [ 31 | { 32 | loader: 'babel-loader', 33 | options: { 34 | presets: [ 35 | '@babel/preset-env', 36 | '@babel/preset-react', 37 | '@babel/preset-typescript', 38 | ], 39 | }, 40 | }, 41 | ], 42 | }, 43 | { 44 | test: /\.(sa|sc|c)ss$/, 45 | use: ['style-loader', 'css-loader', 'sass-loader'], 46 | }, 47 | ], 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /cmds/clients/contestcli/descriptors/debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": [ 3 | { 4 | "name": "randecho", 5 | "label": "some label" 6 | } 7 | ] 8 | } 9 | 10 | -------------------------------------------------------------------------------- /cmds/clients/contestcli/descriptors/fwtesting/lspci.json: -------------------------------------------------------------------------------- 1 | { 2 | "JobName": "lspci test job", 3 | "Runs": 1, 4 | "RunInterval": "3s", 5 | "Tags": ["lscpi"], 6 | "TestDescriptors": [ 7 | { 8 | "TargetManagerName": "TargetList", 9 | "TargetManagerAcquireParameters": { 10 | "Targets": [ 11 | { 12 | "FQDN": "dut1", 13 | "ID": "12345" 14 | } 15 | ] 16 | }, 17 | "TargetManagerReleaseParameters": { 18 | }, 19 | "TestFetcherName": "literal", 20 | "TestFetcherFetchParameters": { 21 | "TestName": "Literal test", 22 | "Steps": [ 23 | { 24 | "name": "sshcmd", 25 | "parameters": { 26 | "user": ["sesame"], 27 | "host": ["{{ .FQDN }}"], 28 | "password": [""], 29 | "executable": ["lspci"], 30 | "expect": ["DDRIO Global Broadcast"] 31 | } 32 | } 33 | ] 34 | } 35 | } 36 | ], 37 | "Reporting": { 38 | "RunReporters": [ 39 | { 40 | "Name": "TargetSuccess", 41 | "Parameters": { 42 | "SuccessExpression": ">80%" 43 | } 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cmds/clients/contestcli/descriptors/start-literal-with-cleanup.json: -------------------------------------------------------------------------------- 1 | { 2 | "JobName": "test job", 3 | "Runs": 1, 4 | "RunInterval": "3s", 5 | "Tags": [ 6 | "test", 7 | "csv" 8 | ], 9 | "TestDescriptors": [ 10 | { 11 | "TargetManagerName": "TargetList", 12 | "TargetManagerAcquireParameters": { 13 | "Targets": [ 14 | { 15 | "FQDN": "example.org", 16 | "ID": "1234" 17 | } 18 | ] 19 | }, 20 | "TargetManagerReleaseParameters": {}, 21 | "TestFetcherName": "literal", 22 | "TestFetcherFetchParameters": { 23 | "TestName": "Literal test", 24 | "Steps": [ 25 | { 26 | "name": "cmd", 27 | "label": "echoStep", 28 | "parameters": { 29 | "executable": [ 30 | "/bin/echo" 31 | ], 32 | "args": [ 33 | "Title={{ Title .FQDN }}, ToUpper={{ ToUpper .FQDN }}" 34 | ] 35 | } 36 | } 37 | ] 38 | }, 39 | "CleanupFetcherName": "literal", 40 | "CleanupFetcherFetchParameters": { 41 | "Steps": [ 42 | { 43 | "name": "cmd", 44 | "label": "echoCleanup", 45 | "parameters": { 46 | "executable": [ 47 | "/bin/echo" 48 | ], 49 | "args": [ 50 | "THIS IS A CLEANUP STEP" 51 | ] 52 | } 53 | } 54 | ] 55 | } 56 | } 57 | ], 58 | "Reporting": { 59 | "RunReporters": [ 60 | { 61 | "Name": "TargetSuccess", 62 | "Parameters": { 63 | "SuccessExpression": "=100%" 64 | } 65 | } 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cmds/clients/contestcli/descriptors/start-literal.json: -------------------------------------------------------------------------------- 1 | { 2 | "JobName": "test job", 3 | "Runs": 3, 4 | "RunInterval": "3s", 5 | "Tags": [ 6 | "test", 7 | "csv" 8 | ], 9 | "TestDescriptors": [ 10 | { 11 | "TargetManagerName": "TargetList", 12 | "TargetManagerAcquireParameters": { 13 | "Targets": [ 14 | { 15 | "FQDN": "example.org", 16 | "ID": "1234" 17 | } 18 | ] 19 | }, 20 | "TargetManagerReleaseParameters": {}, 21 | "TestFetcherName": "literal", 22 | "TestFetcherFetchParameters": { 23 | "TestName": "Literal test", 24 | "Steps": [ 25 | { 26 | "name": "cmd", 27 | "label": "somelabel", 28 | "parameters": { 29 | "executable": [ 30 | "echo" 31 | ], 32 | "args": [ 33 | "Title={{ Title .FQDN }}, ToUpper={{ ToUpper .FQDN }}" 34 | ] 35 | } 36 | } 37 | ] 38 | } 39 | } 40 | ], 41 | "Reporting": { 42 | "RunReporters": [ 43 | { 44 | "Name": "TargetSuccess", 45 | "Parameters": { 46 | "SuccessExpression": ">80%" 47 | } 48 | }, 49 | { 50 | "Name": "Noop" 51 | } 52 | ], 53 | "FinalReporters": [ 54 | { 55 | "Name": "noop" 56 | } 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cmds/clients/contestcli/descriptors/start-literal.yaml: -------------------------------------------------------------------------------- 1 | JobName: test job 2 | Runs: 3 3 | RunInterval: 3s 4 | Tags: [test, csv] 5 | TestDescriptors: 6 | - TargetManagerName: csvfiletargetmanager 7 | TargetManagerAcquireParameters: 8 | FileURI: hosts.csv 9 | Shuffle: true 10 | MinNumberDevices: 1 11 | MaxNumberDevices: 1 12 | TargetManagerReleaseParameters: 13 | TestFetcherName: literal 14 | TestFetcherFetchParameters: 15 | TestName: literal test 16 | Steps: 17 | - name: cmd 18 | label: somelabel 19 | parameters: 20 | executable: [echo] 21 | args: ["Title={{ Title .FQDN }}, ToUpper={{ ToUpper .FQDN }}"] 22 | emit_stdout: [true] 23 | emit_stderr: [true] 24 | Reporting: 25 | RunReporters: 26 | - name: TargetSuccess 27 | parameters: 28 | SuccessExpression: ">80%" 29 | - name: noop 30 | FinalReporters: 31 | - name: noop 32 | -------------------------------------------------------------------------------- /cmds/clients/contestcli/descriptors/start.json: -------------------------------------------------------------------------------- 1 | { 2 | "JobName": "test job", 3 | "Runs": 1, 4 | "RunInterval": "5s", 5 | "Tags": [ 6 | "test", 7 | "csv" 8 | ], 9 | "TestDescriptors": [ 10 | { 11 | "TargetManagerName": "CSVFileTargetManager", 12 | "TargetManagerAcquireParameters": { 13 | "FileURI": "hosts.csv", 14 | "MinNumberDevices": 2, 15 | "MaxNumberDevices": 4, 16 | "HostPrefixes": [] 17 | }, 18 | "TargetManagerReleaseParameters": {}, 19 | "TestFetcherName": "URI", 20 | "TestFetcherFetchParameters": { 21 | "TestName": "RackSwitchProvisioning", 22 | "URI": "test_samples/randecho.json" 23 | } 24 | }, 25 | { 26 | "TargetManagerName": "CSVFileTargetManager", 27 | "TargetManagerAcquireParameters": { 28 | "FileURI": "hosts.csv", 29 | "MinNumberDevices": 2, 30 | "MaxNumberDevices": 4, 31 | "HostPrefixes": [] 32 | }, 33 | "TargetManagerReleaseParameters": {}, 34 | "TestFetcherName": "URI", 35 | "TestFetcherFetchParameters": { 36 | "TestName": "RackProvisioning", 37 | "URI": "test_samples/randecho.json" 38 | } 39 | } 40 | ], 41 | "Reporting": { 42 | "RunReporters": [ 43 | { 44 | "Name": "TargetSuccess", 45 | "Parameters": { 46 | "SuccessExpression": ">80%" 47 | } 48 | }, 49 | { 50 | "Name": "Noop" 51 | } 52 | ] 53 | } 54 | } -------------------------------------------------------------------------------- /cmds/clients/contestcli/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | 12 | "github.com/linuxboot/contest/cmds/clients/contestcli/cli" 13 | ) 14 | 15 | // Unauthenticated, unencrypted sample HTTP client for ConTest. 16 | // Requires the `httplistener` plugin for the API listener. 17 | // 18 | // Usage examples: 19 | // Start a job with the provided job description from a JSON file 20 | // ./contestcli start start.json 21 | // 22 | // Get the status of a job whose ID is 10 23 | // ./contestcli status 10 24 | // 25 | // List all the jobs: 26 | // ./contestcli list 27 | // 28 | // List all the failed jobs: 29 | // ./contestcli list -state JobStateFailed 30 | // 31 | // List all the failed jobs with tags "foo" and "bar": 32 | // ./contestcli list -state JobStateFailed -tags foo,bar 33 | 34 | func main() { 35 | if err := cli.CLIMain(os.Args[0], os.Args[1:], os.Stdout); err != nil { 36 | fmt.Fprintf(os.Stderr, "%v\n", err) 37 | os.Exit(1) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cmds/contest-generator/contest.go.template: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | {{/* This file is the template source. The following comment obviously doesn't apply here */ -}} 7 | // This is a generated file, edits should be made in the corresponding source 8 | // file, and this file regenerated using 9 | // `contest-generator --from {{ .ConfigFile }}` 10 | // followed by 11 | // `gofmt -w contest.go` 12 | 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | "os" 18 | "os/signal" 19 | "syscall" 20 | 21 | "github.com/linuxboot/contest/cmds/contest/server" 22 | 23 | // the targetmanager plugins 24 | {{- range $plugin := .TargetManagers }} 25 | {{ $plugin.ToAlias}} "{{ $plugin }}" 26 | {{- end }} 27 | 28 | // the testfetcher plugins 29 | {{- range $plugin := .TestFetchers }} 30 | {{ $plugin.ToAlias}} "{{ $plugin }}" 31 | {{- end }} 32 | 33 | // the teststep plugins 34 | {{- range $plugin := .TestSteps }} 35 | {{ $plugin.ToAlias}} "{{ $plugin.Path }}" 36 | {{- end }} 37 | 38 | // the reporter plugins 39 | {{- range $plugin := .Reporters }} 40 | {{ $plugin.ToAlias}} "{{ $plugin }}" 41 | {{- end }} 42 | ) 43 | 44 | func getPluginConfig() *server.PluginConfig { 45 | var pc server.PluginConfig 46 | 47 | {{- range $name := .TargetManagers }} 48 | pc.TargetManagerLoaders = append(pc.TargetManagerLoaders, {{ $name.ToAlias }}.Load) 49 | {{- end }} 50 | 51 | {{- range $name := .TestFetchers }} 52 | pc.TestFetcherLoaders = append(pc.TestFetcherLoaders, {{ $name.ToAlias }}.Load) 53 | {{- end }} 54 | 55 | {{- range $name := .TestSteps }} 56 | pc.TestStepLoaders = append(pc.TestStepLoaders, {{ $name.ToAlias }}.Load) 57 | {{- end }} 58 | 59 | {{- range $name := .Reporters }} 60 | pc.ReporterLoaders = append(pc.ReporterLoaders, {{ $name.ToAlias }}.Load) 61 | {{- end }} 62 | 63 | return &pc 64 | } 65 | 66 | func main() { 67 | sigs := make(chan os.Signal, 1) 68 | defer close(sigs) 69 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) 70 | if err := server.Main(getPluginConfig(), os.Args[0], os.Args[1:], sigs); err != nil { 71 | fmt.Fprintf(os.Stderr, "%v\n", err) 72 | os.Exit(1) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cmds/contest-generator/core-plugins.yml: -------------------------------------------------------------------------------- 1 | targetmanagers: 2 | - path: github.com/linuxboot/contest/plugins/targetmanagers/targetlist 3 | - path: github.com/linuxboot/contest/plugins/targetmanagers/csvtargetmanager 4 | 5 | testfetchers: 6 | - path: github.com/linuxboot/contest/plugins/testfetchers/literal 7 | - path: github.com/linuxboot/contest/plugins/testfetchers/uri 8 | 9 | teststeps: 10 | - path: github.com/linuxboot/contest/plugins/teststeps/cmd 11 | alias: ts_cmd 12 | - path: github.com/linuxboot/contest/plugins/teststeps/cpucmd 13 | - path: github.com/linuxboot/contest/plugins/teststeps/sshcmd 14 | - path: github.com/linuxboot/contest/plugins/teststeps/sleep 15 | - path: github.com/linuxboot/contest/plugins/teststeps/exec 16 | - path: github.com/linuxboot/contest/plugins/teststeps/echo 17 | - path: github.com/linuxboot/contest/plugins/teststeps/randecho 18 | - path: github.com/linuxboot/contest/plugins/teststeps/gathercmd 19 | 20 | reporters: 21 | - path: github.com/linuxboot/contest/plugins/reporters/targetsuccess 22 | - path: github.com/linuxboot/contest/plugins/reporters/noop 23 | -------------------------------------------------------------------------------- /cmds/contest-generator/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "log" 11 | "os" 12 | "path" 13 | "text/template" 14 | 15 | flag "github.com/spf13/pflag" 16 | ) 17 | 18 | var ( 19 | flagTemplate = flag.StringP("template", "t", "contest.go.template", "Template file name") 20 | flagOutfile = flag.StringP("outfile", "o", "", "Output file path") 21 | flagFromFile = flag.StringP("from", "f", "core-plugins.yml", "File name to get the plugin list from, in YAML format") 22 | ) 23 | 24 | func usage() { 25 | fmt.Fprintf(flag.CommandLine.Output(), 26 | "%s [--template tpl] [--outfile out] [--from pluginlist.yml]\n", 27 | os.Args[0], 28 | ) 29 | flag.PrintDefaults() 30 | } 31 | 32 | func main() { 33 | flag.Usage = usage 34 | flag.Parse() 35 | 36 | data, err := os.ReadFile(*flagTemplate) 37 | if err != nil { 38 | log.Fatalf("Failed to read template file '%s': %v", *flagTemplate, err) 39 | } 40 | t, err := template.New("contest").Parse(string(data)) 41 | if err != nil { 42 | log.Fatalf("Template parsing failed: %v", err) 43 | } 44 | cfg, err := readConfig(*flagFromFile) 45 | if err != nil { 46 | log.Fatalf("Error parsing configuration file '%s': %v", *flagFromFile, err) 47 | } 48 | outfile := *flagOutfile 49 | if outfile == "" { 50 | tmpdir, err := os.MkdirTemp("", "contest") 51 | if err != nil { 52 | log.Fatalf("Cannot create temporary directory: %v", err) 53 | } 54 | outfile = path.Join(tmpdir, "contest.go") 55 | } 56 | 57 | log.Printf("Generating output file '%s' with the following plugins):\n%s", outfile, cfg) 58 | outFD, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, 0644) 59 | if err != nil { 60 | log.Fatalf("Failed to create output file '%s': %v", outfile, err) 61 | } 62 | defer func() { 63 | if err := outFD.Close(); err != nil { 64 | log.Printf("Error while closing file descriptor for '%s': %v", outfile, err) 65 | } 66 | }() 67 | if err := t.Execute(outFD, cfg); err != nil { 68 | log.Fatalf("Template execution failed: %v", err) 69 | } 70 | log.Printf("Generated file '%s'. You can build it by running 'go build' in the output directory.", outfile) 71 | fmt.Println(path.Dir(outfile)) 72 | } 73 | -------------------------------------------------------------------------------- /cmds/contest/hosts.csv: -------------------------------------------------------------------------------- 1 | 1234,compute1234,172.16.5.6, 2 | 2345,storage2345,10.10.10.123, 3 | 3456,machinelearning3456,192.168.123.231, 4 | 10,uselesshost10,10.0.0.10, 5 | 20,uselesshost20,10.0.0.20, 6 | 30,uselesshost30,10.0.0.30, 7 | -------------------------------------------------------------------------------- /cmds/contest/test_samples/randecho.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": [ 3 | { 4 | "name": "echo", 5 | "label": "thefirstrandecho", 6 | "parameters": { 7 | "text": ["some very random text"] 8 | } 9 | }, 10 | { 11 | "name": "randecho", 12 | "label": "thesecondrandecho", 13 | "parameters": { 14 | "text": ["some very random text"] 15 | } 16 | }, 17 | { 18 | "name": "echo", 19 | "label": "thethirdrandecho", 20 | "parameters": { 21 | "text": ["some very random text"] 22 | } 23 | } 24 | ] 25 | } 26 | 27 | -------------------------------------------------------------------------------- /cmds/contest/test_samples/sleep.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": [ 3 | { 4 | "name": "cmd", 5 | "label": "sleeping...", 6 | "parameters": { 7 | "executable": ["/bin/sleep"], 8 | "args": ["60"] 9 | } 10 | } 11 | ] 12 | } 13 | 14 | -------------------------------------------------------------------------------- /cmds/contest/test_samples/slowecho.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": [ 3 | { 4 | "name": "slowecho", 5 | "label": "aslowecho", 6 | "parameters": { 7 | "sleep": ["3"], 8 | "text": ["nel mezzo del cammin"] } 9 | } 10 | ] 11 | } 12 | 13 | -------------------------------------------------------------------------------- /cmds/contest/test_samples/sshhostname.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": [ 3 | { 4 | "name": "sshcmd", 5 | "label": "some label", 6 | "parameters": { 7 | "user": ["your_username"], 8 | "host": ["{{ .FQDN }}"], 9 | "private_key_file": ["/home/user/.ssh/id_rsa"], 10 | "executable": ["ping"], 11 | "args": ["-c1", "-W2", "www.{{ ToLower .FQDN }}"] 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /cmds/contest/test_samples/terminalexpect.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": [ 3 | { 4 | "name": "terminalexpect", 5 | "label": "wait for starting", 6 | "parameters": { 7 | "port": ["/dev/ttyUSB0"], 8 | "speed": ["115200"], 9 | "match": ["ramstage starting"], 10 | "timeout": ["30s"] 11 | } 12 | }, 13 | { 14 | "name": "terminalexpect", 15 | "label": "wait for trying boot configuration", 16 | "parameters": { 17 | "port": ["/dev/ttyUSB0"], 18 | "speed": ["115200"], 19 | "match": ["Trying boot configuration"], 20 | "timeout": ["1m"] 21 | } 22 | }, 23 | { 24 | "name": "terminalexpect", 25 | "label": "wait for login", 26 | "parameters": { 27 | "port": ["/dev/ttyUSB0"], 28 | "speed": ["115200"], 29 | "match": ["up-UP-APL01 login:"], 30 | "timeout": ["1m"] 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /cmds/exec_agent/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package main 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | "log" 12 | "os" 13 | "path" 14 | "syscall" 15 | "time" 16 | ) 17 | 18 | var ( 19 | flagSet *flag.FlagSet 20 | flagTimeQuota *time.Duration 21 | flagDebug *bool 22 | ) 23 | 24 | func initFlags(cmd string) { 25 | flagSet = flag.NewFlagSet(cmd, flag.ContinueOnError) 26 | flagTimeQuota = flagSet.Duration("time-quota", 0, "Time quota until the process self-destructs; 0 means infinite") 27 | flagDebug = flagSet.Bool("debug", false, "Output logs and errors in foreground, otherwise close stderr") 28 | 29 | flagSet.Usage = func() { 30 | fmt.Fprintf(flagSet.Output(), 31 | `Usage: 32 | 33 | %s [flags] command 34 | 35 | Commands: 36 | start /path/to/binary 37 | start a new binary and detach from the controlling TTY if any 38 | 39 | Flags: 40 | `, path.Base(cmd)) 41 | flagSet.PrintDefaults() 42 | } 43 | } 44 | 45 | func main() { 46 | initFlags(os.Args[0]) 47 | 48 | if err := flagSet.Parse(os.Args[1:]); err != nil { 49 | if err == flag.ErrHelp { 50 | return 51 | } 52 | log.Fatalf("failed to parse args: %v", err) 53 | } 54 | 55 | // when not run as debug, redirect stdin and stderr to /dev/null 56 | if !*flagDebug { 57 | null, err := os.Open(os.DevNull) 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | defer null.Close() 62 | 63 | if err := syscall.Dup2(int(null.Fd()), 0); err != nil { 64 | log.Fatalf("failed stdin dup2: %v", err) 65 | } 66 | if err := syscall.Dup2(int(null.Fd()), 2); err != nil { 67 | log.Fatalf("failed stderr dup2: %v", err) 68 | } 69 | } 70 | 71 | if err := run(); err != nil { 72 | log.Fatalf("execution failed: %v", err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /db/rdbms/migration/0001_add_extended_descriptor.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Facebook, Inc. and its affiliates. 2 | -- 3 | -- This source code is licensed under the MIT license found in the 4 | -- LICENSE file in the root directory of this source tree. 5 | -- +goose Up 6 | ALTER TABLE jobs ADD COLUMN extended_descriptor text; 7 | 8 | -- +goose Down 9 | -- Down migration is not supported and it's a no-op 10 | -------------------------------------------------------------------------------- /db/rdbms/migration/0003_add_jobs_state_column.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Facebook, Inc. and its affiliates. 2 | -- 3 | -- This source code is licensed under the MIT license found in the 4 | -- LICENSE file in the root directory of this source tree. 5 | 6 | -- +goose Up 7 | 8 | ALTER TABLE jobs ADD COLUMN state tinyint DEFAULT 0 AFTER extended_descriptor; 9 | CREATE INDEX job_state ON jobs (state, job_id); 10 | 11 | -- Populate the column. 12 | 13 | SET SQL_BIG_SELECTS=1; 14 | 15 | UPDATE 16 | jobs 17 | INNER JOIN 18 | ( -- This query retrieves job state event by its id. 19 | SELECT 20 | jobs.job_id AS job_id, 21 | fe2.event_name AS state 22 | FROM 23 | jobs 24 | INNER JOIN 25 | ( -- This query finds id of the last job state event for each job. 26 | SELECT 27 | job_id, 28 | MAX(event_id) AS max_event 29 | FROM 30 | framework_events fe 31 | WHERE 32 | fe.event_name IN ("JobStateStarted", "JobStateCompleted", "JobStateFailed", 33 | "JobStatePaused", "JobStatePauseFailed", "JobStateCancelling", 34 | "JobStateCancelled", "JobStateCancellationFailed") 35 | GROUP BY 36 | job_id 37 | ) fe1 38 | ON 39 | fe1.job_id = jobs.job_id 40 | INNER JOIN 41 | framework_events fe2 42 | ON 43 | fe2.event_id = fe1.max_event 44 | ) tt 45 | ON 46 | jobs.job_id = tt.job_id 47 | SET 48 | jobs.state = 49 | -- Translate string to numeric representation. 50 | IF(tt.state = "JobStateStarted", 1, 51 | IF(tt.state = "JobStateCompleted", 2, 52 | IF(tt.state = "JobStateFailed", 3, 53 | IF(tt.state = "JobStatePaused", 4, 54 | IF(tt.state = "JobStatePauseFailed", 5, 55 | IF(tt.state = "JobStateCancelling", 6, 56 | IF(tt.state = "JobStateCancelled", 7, 57 | IF(tt.state = "JobStateCancellationFailed", 8, 58 | 0)))))))); 59 | 60 | -- +goose Down 61 | 62 | DROP INDEX job_state ON jobs 63 | ALTER TABLE jobs DROP COLUMN state; 64 | -------------------------------------------------------------------------------- /db/rdbms/migration/0004_add_job_tags_table.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Facebook, Inc. and its affiliates. 2 | -- 3 | -- This source code is licensed under the MIT license found in the 4 | -- LICENSE file in the root directory of this source tree. 5 | 6 | -- +goose Up 7 | 8 | CREATE TABLE job_tags ( 9 | job_id BIGINT NOT NULL, 10 | tag VARCHAR(32), 11 | PRIMARY KEY (job_id, tag), 12 | KEY(tag) 13 | ); 14 | 15 | -- Extract tags from the descriptor field. 16 | SET SQL_BIG_SELECTS=1; 17 | INSERT INTO 18 | job_tags 19 | SELECT 20 | job_id, 21 | job_tags.tag AS tag 22 | FROM 23 | jobs, 24 | JSON_TABLE(jobs.descriptor, '$.Tags[*]' COLUMNS (tag TEXT PATH '$[0]')) AS job_tags; 25 | 26 | -- +goose Down 27 | 28 | DROP TABLE job_tags; 29 | -------------------------------------------------------------------------------- /db/rdbms/migration/0005_event_payload_mediumtext.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Facebook, Inc. and its affiliates. 2 | -- 3 | -- This source code is licensed under the MIT license found in the 4 | -- LICENSE file in the root directory of this source tree. 5 | 6 | -- +goose Up 7 | 8 | ALTER TABLE test_events MODIFY COLUMN payload MEDIUMTEXT; 9 | ALTER TABLE framework_events MODIFY COLUMN payload MEDIUMTEXT; 10 | 11 | -- +goose Down 12 | 13 | ALTER TABLE test_events MODIFY COLUMN payload TEXT; 14 | ALTER TABLE framework_events MODIFY COLUMN payload TEXT; 15 | -------------------------------------------------------------------------------- /db/rdbms/migration/0006_add_indices.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Facebook, Inc. and its affiliates. 2 | -- 3 | -- This source code is licensed under the MIT license found in the 4 | -- LICENSE file in the root directory of this source tree. 5 | 6 | -- +goose Up 7 | 8 | ALTER TABLE test_events ADD INDEX fetch_index_0 (job_id, run_id, test_name); 9 | ALTER TABLE framework_events ADD INDEX fetch_index_0 (job_id, event_name); 10 | ALTER TABLE run_reports ADD INDEX job_id_idx (job_id); 11 | ALTER TABLE final_reports ADD INDEX job_id_idx (job_id); 12 | 13 | -- +goose Down 14 | 15 | ALTER TABLE test_events DROP INDEX fetch_index_0; 16 | ALTER TABLE framework_events DROP INDEX fetch_index_0; 17 | ALTER TABLE run_reports DROP INDEX job_id_idx; 18 | ALTER TABLE final_reports DROP INDEX job_id_idx; 19 | -------------------------------------------------------------------------------- /db/rdbms/migration/0007_add_test_attempt_column.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Facebook, Inc. and its affiliates. 2 | -- 3 | -- This source code is licensed under the MIT license found in the 4 | -- LICENSE file in the root directory of this source tree. 5 | 6 | -- +goose Up 7 | 8 | ALTER TABLE test_events ADD COLUMN test_attempt INTEGER UNSIGNED NOT NULL DEFAULT 0; 9 | 10 | -- +goose Down 11 | 12 | ALTER TABLE test_events DROP COLUMN test_attempt; -------------------------------------------------------------------------------- /db/rdbms/schema/v0/create_contest_db.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Facebook, Inc. and its affiliates. 2 | -- 3 | -- This source code is licensed under the MIT license found in the 4 | -- LICENSE file in the root directory of this source tree. 5 | 6 | CREATE TABLE test_events ( 7 | event_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, 8 | job_id BIGINT(20) NOT NULL, 9 | run_id BIGINT(20) NOT NULL, 10 | test_name VARCHAR(32) NULL, 11 | test_step_label VARCHAR(32) NULL, 12 | event_name VARCHAR(32) NULL, 13 | target_name VARCHAR(64) NULL, 14 | target_id VARCHAR(64) NULL, 15 | payload TEXT NULL, 16 | emit_time TIMESTAMP NOT NULL, 17 | PRIMARY KEY (event_id) 18 | ); 19 | 20 | CREATE TABLE framework_events ( 21 | event_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, 22 | job_id BIGINT(20) NOT NULL, 23 | event_name VARCHAR(32) NULL, 24 | payload TEXT NULL, 25 | emit_time TIMESTAMP NOT NULL, 26 | PRIMARY KEY (event_id) 27 | ); 28 | 29 | CREATE TABLE run_reports ( 30 | report_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, 31 | job_id BIGINT(20) NOT NULL, 32 | run_id BIGINT(20) NOT NULL, 33 | reporter_name VARCHAR(32) NOT NULL, 34 | success TINYINT(1) NULL, 35 | report_time TIMESTAMP NOT NULL, 36 | data TEXT NOT NULL, 37 | PRIMARY KEY (report_id) 38 | ); 39 | 40 | CREATE TABLE final_reports ( 41 | report_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, 42 | job_id BIGINT(20) NOT NULL, 43 | success TINYINT(1) NULL, 44 | reporter_name VARCHAR(32) NOT NULL, 45 | report_time TIMESTAMP NOT NULL, 46 | data TEXT NOT NULL, 47 | PRIMARY KEY (report_id) 48 | ); 49 | 50 | CREATE TABLE jobs ( 51 | job_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, 52 | name VARCHAR(64) NOT NULL, 53 | requestor VARCHAR(32) NOT NULL, 54 | server_id VARCHAR(64) NOT NULL, 55 | request_time TIMESTAMP NOT NULL, 56 | descriptor TEXT NOT NULL, 57 | teststeps TEXT, 58 | PRIMARY KEY (job_id) 59 | ); 60 | 61 | CREATE TABLE locks ( 62 | target_id VARCHAR(64) NOT NULL, 63 | job_id BIGINT(20) UNSIGNED NOT NULL, 64 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 65 | expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 66 | valid BOOL NOT NULL DEFAULT TRUE, 67 | PRIMARY KEY (target_id) 68 | ); 69 | -------------------------------------------------------------------------------- /docker-compose.mariadb.yml: -------------------------------------------------------------------------------- 1 | services: 2 | contest: 3 | build: 4 | context: . 5 | dockerfile: docker/contest/Dockerfile 6 | command: bash -c "cd /go/src/github.com/linuxboot/contest/cmds/contest/ && go run . -dbURI 'contest:contest@tcp(dbstorage:3306)/contest_integ?parseTime=true'" 7 | ports: 8 | - 8080:8080 9 | depends_on: 10 | - mariadb 11 | networks: 12 | - net 13 | 14 | mariadb: 15 | environment: 16 | - MARIADB_RANDOM_ROOT_PASSWORD=true 17 | build: 18 | context: . 19 | dockerfile: docker/mariadb/Dockerfile 20 | ports: 21 | - 3306:3306 22 | networks: 23 | net: 24 | aliases: 25 | - dbstorage 26 | 27 | networks: 28 | net: 29 | -------------------------------------------------------------------------------- /docker/contest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18 2 | 3 | RUN apt-get update && apt-get install -y mariadb-client openssh-server 4 | 5 | # setup sshd for some plugin tests 6 | RUN ssh-keygen -q -t rsa -f /root/.ssh/id_rsa -N "" 7 | RUN cat /root/.ssh/id_rsa.pub > /root/.ssh/authorized_keys 8 | 9 | WORKDIR ${GOPATH}/src/github.com/linuxboot/contest 10 | COPY . . 11 | RUN go get -t -v ./... 12 | RUN chmod a+x docker/contest/tests.sh 13 | -------------------------------------------------------------------------------- /docker/mariadb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mariadb:10.6 2 | 3 | # Configure golang environment to run migration against database 4 | ARG TARGETARCH 5 | RUN apt-get update && apt-get install -y curl && apt-get clean 6 | RUN curl -L https://golang.org/dl/go1.16.2.linux-$TARGETARCH.tar.gz | tar xzf - 7 | ENV GOROOT=/go 8 | ENV PATH=$PATH:/go/bin 9 | 10 | RUN mkdir /home/mysql && chown mysql:mysql /home/mysql 11 | 12 | USER mysql 13 | 14 | COPY --chown=mysql:mysql . /home/mysql/contest 15 | 16 | WORKDIR /home/mysql 17 | 18 | # Pre-build the migration script, make sure it builds. 19 | RUN cd /home/mysql/contest && \ 20 | go build github.com/linuxboot/contest/tools/migration/rdbms 21 | 22 | # All scripts in docker-entrypoint-initdb.d/ are automatically 23 | # executed during container startup 24 | COPY docker/mariadb/initdb.sql /docker-entrypoint-initdb.d/ 25 | COPY db/rdbms/schema/v0/create_contest_db.sql / 26 | 27 | # Run all known migrations at the time of the creation of the container. 28 | # From container documentation: 29 | # """ 30 | # When a container is started for the first time, a new database with the 31 | # specified name will be created and initialized with the provided configuration 32 | # variables. Furthermore, it will execute files with extensions .sh, .sql and .sql.gz 33 | # that are found in /docker-entrypoint-initdb.d. Files will be executed in alphabetical 34 | # order. 35 | # """ 36 | COPY docker/mariadb/migration.sh /docker-entrypoint-initdb.d/ 37 | -------------------------------------------------------------------------------- /docker/mariadb/initdb.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Facebook, Inc. and its affiliates. 2 | -- 3 | -- This source code is licensed under the MIT license found in the 4 | -- LICENSE file in the root directory of this source tree. 5 | 6 | CREATE DATABASE contest; 7 | CREATE DATABASE contest_integ; 8 | 9 | /* 10 | * use mysql_native_password as auth method because caching_sha2_password is 11 | * not supported by mariadb and it's the default for MySQL (starting from 8.0). 12 | * See https://mariadb.com/kb/en/library/authentication-plugin-sha-256/ 13 | */ 14 | CREATE USER 'contest'@'%' IDENTIFIED WITH mysql_native_password AS PASSWORD('contest'); 15 | GRANT ALL ON contest.* TO 'contest'@'%'; 16 | GRANT ALL ON contest_integ.* TO 'contest'@'%'; 17 | FLUSH PRIVILEGES; 18 | 19 | USE contest; 20 | source /create_contest_db.sql 21 | 22 | USE contest_integ; 23 | source /create_contest_db.sql 24 | -------------------------------------------------------------------------------- /docker/mariadb/migration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) Facebook, Inc. and its affiliates. 4 | # 5 | # This source code is licensed under the MIT license found in the 6 | # LICENSE file in the root directory of this source tree. 7 | 8 | set -eo pipefail 9 | echo "Running database migrations..." 10 | cd /home/mysql/contest 11 | # Migrate main db and integration tests db 12 | go run tools/migration/rdbms/main.go -dbURI "contest:contest@unix(/var/run/mysqld/mysqld.sock)/contest?parseTime=true" -dir db/rdbms/migration up 13 | go run tools/migration/rdbms/main.go -dbURI "contest:contest@unix(/var/run/mysqld/mysqld.sock)/contest_integ?parseTime=true" -dir db/rdbms/migration up 14 | -------------------------------------------------------------------------------- /docker/mongo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mongo:5.0 2 | 3 | COPY docker/mongo/initdb.js /docker-entrypoint-initdb.d/ 4 | -------------------------------------------------------------------------------- /docker/mongo/initdb.js: -------------------------------------------------------------------------------- 1 | #!/bin/env mongo 2 | 3 | // note: mongo creates the database when getting it 4 | db = db.getSiblingDB('admin-server-db') 5 | db.createCollection('logs') 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docker/mysql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:8.0-debian 2 | 3 | # Configure golang environment to run migration against database 4 | ARG TARGETARCH 5 | RUN apt-get update && apt-get install -y curl && apt-get clean 6 | RUN apt-get install -y git 7 | RUN curl -L https://golang.org/dl/go1.18.linux-$TARGETARCH.tar.gz | tar xzf - 8 | ENV GOROOT=/go 9 | ENV PATH=$PATH:/go/bin 10 | 11 | RUN mkdir /home/mysql && chown mysql:mysql /home/mysql 12 | 13 | USER mysql 14 | 15 | COPY --chown=mysql:mysql . /home/mysql/contest 16 | 17 | WORKDIR /home/mysql 18 | 19 | # Pre-build the migration script, make sure it builds. 20 | RUN cd /home/mysql/contest && \ 21 | go build github.com/linuxboot/contest/tools/migration/rdbms 22 | 23 | # All scripts in docker-entrypoint-initdb.d/ are automatically 24 | # executed during container startup 25 | COPY docker/mysql/initdb.sql /docker-entrypoint-initdb.d/ 26 | COPY db/rdbms/schema/v0/create_contest_db.sql / 27 | 28 | # Run all known migrations at the time of the creation of the container. 29 | # From container documentation: 30 | # """ 31 | # When a container is started for the first time, a new database with the 32 | # specified name will be created and initialized with the provided configuration 33 | # variables. Furthermore, it will execute files with extensions .sh, .sql and .sql.gz 34 | # that are found in /docker-entrypoint-initdb.d. Files will be executed in alphabetical 35 | # order. 36 | # """ 37 | COPY docker/mysql/migration.sh /docker-entrypoint-initdb.d/ 38 | -------------------------------------------------------------------------------- /docker/mysql/initdb.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Facebook, Inc. and its affiliates. 2 | -- 3 | -- This source code is licensed under the MIT license found in the 4 | -- LICENSE file in the root directory of this source tree. 5 | 6 | CREATE DATABASE contest; 7 | CREATE DATABASE contest_integ; 8 | 9 | /* 10 | * use mysql_native_password as auth method because caching_sha2_password is 11 | * not supported by mariadb and it's the default for MySQL (starting from 8.0). 12 | * See https://mariadb.com/kb/en/library/authentication-plugin-sha-256/ 13 | */ 14 | CREATE USER 'contest'@'%' IDENTIFIED WITH mysql_native_password BY 'contest'; 15 | GRANT ALL ON contest.* TO 'contest'@'%'; 16 | GRANT ALL ON contest_integ.* TO 'contest'@'%'; 17 | FLUSH PRIVILEGES; 18 | 19 | USE contest; 20 | source /create_contest_db.sql 21 | 22 | USE contest_integ; 23 | source /create_contest_db.sql 24 | -------------------------------------------------------------------------------- /docker/mysql/migration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) Facebook, Inc. and its affiliates. 4 | # 5 | # This source code is licensed under the MIT license found in the 6 | # LICENSE file in the root directory of this source tree. 7 | 8 | set -eo pipefail 9 | echo "Running database migrations..." 10 | cd /home/mysql/contest 11 | # Migrate main db and integration tests db 12 | go run tools/migration/rdbms/main.go -dbURI "contest:contest@unix(/var/run/mysqld/mysqld.sock)/contest?parseTime=true" -dir db/rdbms/migration up 13 | go run tools/migration/rdbms/main.go -dbURI "contest:contest@unix(/var/run/mysqld/mysqld.sock)/contest_integ?parseTime=true" -dir db/rdbms/migration up 14 | 15 | -------------------------------------------------------------------------------- /pkg/api/listener.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package api 7 | 8 | import "context" 9 | 10 | // Listener defines the interface for an API listener. This is used to 11 | // implement different API transports, like thrift, or gRPC. 12 | type Listener interface { 13 | // Serve is responsible for starting the listener and calling the API 14 | // methods to communicate with the JobManager. 15 | // The channel is used for cancellation, which can be called by the 16 | // JobManager and should be handled by the listener to do a graceful 17 | // shutdown. 18 | Serve(context.Context, *API) error 19 | } 20 | -------------------------------------------------------------------------------- /pkg/api/options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package api 7 | 8 | import ( 9 | "time" 10 | ) 11 | 12 | // Option is an additional argument to method New to change the behavior 13 | // of API processing. 14 | type Option interface { 15 | Apply(*Config) 16 | } 17 | 18 | // Config is a set of knobs to change the behavior of API processing. 19 | // In other words, Config aggregates all the Option-s into one structure. 20 | type Config struct { 21 | // EventTimeout defines time duration for API request to be processed. 22 | // If a request is not processed in time, then an error is returned to the 23 | // client. 24 | EventTimeout time.Duration 25 | 26 | // ServerIDFunc defines a custom server ID in API responses. 27 | ServerIDFunc ServerIDFunc 28 | } 29 | 30 | // OptionEventTimeout defines time duration for API request to be processed. 31 | // If a request is not processed in time, then an error is returned to the 32 | // client. 33 | type OptionEventTimeout time.Duration 34 | 35 | // Apply implements Option. 36 | func (opt OptionEventTimeout) Apply(config *Config) { 37 | config.EventTimeout = (time.Duration)(opt) 38 | } 39 | 40 | // OptionServerID defines a custom server ID in API responses. 41 | type OptionServerID string 42 | 43 | // Apply implements Option. 44 | func (opt OptionServerID) Apply(config *Config) { 45 | config.ServerIDFunc = func() string { 46 | return string(opt) 47 | } 48 | } 49 | 50 | // getConfig converts a set of Option-s into one structure "Config". 51 | func getConfig(opts ...Option) Config { 52 | result := Config{ 53 | EventTimeout: DefaultEventTimeout, 54 | } 55 | for _, opt := range opts { 56 | opt.Apply(&result) 57 | } 58 | return result 59 | } 60 | -------------------------------------------------------------------------------- /pkg/api/values_proxy_context.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package api 7 | 8 | import ( 9 | "context" 10 | "time" 11 | ) 12 | 13 | type valuesProxyContext struct { 14 | valueser context.Context 15 | } 16 | 17 | var _ context.Context = (*valuesProxyContext)(nil) 18 | 19 | // Deadline implements interface context.Context. 20 | func (ctx *valuesProxyContext) Deadline() (time.Time, bool) { 21 | return time.Time{}, false 22 | } 23 | 24 | // Done implements interface context.Context. 25 | func (ctx *valuesProxyContext) Done() <-chan struct{} { 26 | return nil 27 | } 28 | 29 | // Err implements interface context.Context. 30 | func (ctx *valuesProxyContext) Err() error { 31 | return nil 32 | } 33 | 34 | // Value implements interface context.Context. 35 | func (ctx *valuesProxyContext) Value(key any) any { 36 | return ctx.valueser.Value(key) 37 | } 38 | 39 | // newValuesProxyContext returns a context without cancellation/deadline signals, 40 | // but with all the values kept as is. 41 | func newValuesProxyContext(ctx context.Context) context.Context { 42 | ctx = &valuesProxyContext{ 43 | valueser: ctx, 44 | } 45 | return ctx 46 | } 47 | -------------------------------------------------------------------------------- /pkg/config/db.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package config 7 | 8 | // DefaultDBURI represents the default URI used by the rdbms plugin 9 | const DefaultDBURI = "contest:contest@tcp(localhost:3306)/contest?parseTime=true" 10 | -------------------------------------------------------------------------------- /pkg/config/jobdescriptor.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package config 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | // JobDescFormat defines a type for the supported formats for job descriptor configurations. 16 | type JobDescFormat int 17 | 18 | // List of supported job descriptor formats 19 | const ( 20 | JobDescFormatJSON JobDescFormat = iota 21 | JobDescFormatYAML 22 | ) 23 | 24 | // ParseJobDescriptor validates a job descriptor's well-formedness, and returns a 25 | // JSON-formatted descriptor if it was provided in a different format. 26 | // The currently supported format are JSON and YAML. 27 | func ParseJobDescriptor(data []byte, jobDescFormat JobDescFormat) ([]byte, error) { 28 | var ( 29 | jobDesc = make(map[string]interface{}) 30 | ) 31 | switch jobDescFormat { 32 | case JobDescFormatJSON: 33 | if err := json.Unmarshal(data, &jobDesc); err != nil { 34 | return nil, fmt.Errorf("failed to parse JSON job descriptor: %w", err) 35 | } 36 | case JobDescFormatYAML: 37 | if err := yaml.Unmarshal(data, &jobDesc); err != nil { 38 | return nil, fmt.Errorf("failed to parse YAML job descriptor: %w", err) 39 | } 40 | } 41 | 42 | // then marshal the structure back to JSON 43 | jobDescJSON, err := json.MarshalIndent(jobDesc, "", " ") 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to serialize job descriptor to JSON: %w", err) 46 | } 47 | return jobDescJSON, nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/config/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package config 7 | 8 | // MinStorageVersion defines the minimum requires version that contest can run on 9 | const MinStorageVersion = 0 10 | -------------------------------------------------------------------------------- /pkg/config/timeouts.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package config 7 | 8 | import "time" 9 | 10 | // TargetManagerAcquireTimeout represents the maximum time that JobManager should wait 11 | // for the execution of Acquire function from the chosen TargetManager 12 | var TargetManagerAcquireTimeout = 5 * time.Minute 13 | 14 | // TargetManagerReleaseTimeout represents the maximum time that JobManager should wait 15 | // for the execution of Release function from the chosen TargetManager 16 | var TargetManagerReleaseTimeout = 5 * time.Minute 17 | 18 | // TestRunnerMsgTimeout represents the maximum time that any component of the 19 | // TestRunner will wait for the delivery of a message to any other subsystem 20 | // of the TestRunner 21 | var TestRunnerMsgTimeout = 5 * time.Second 22 | 23 | // TestRunnerShutdownTimeout represents the maximum time that the TestRunner will 24 | // wait for all the TestStep to complete after a cancellation signal has been 25 | // delivered 26 | 27 | // TestRunnerShutdownTimeout controls a block of the TestRunner which works as a 28 | // watchdog, i.e. if there are multiple steps that need to return, the timeout is 29 | // reset every time a step returns. The timeout should be handled so that it 30 | // doesn't reset when a TestStep returns. 31 | var TestRunnerShutdownTimeout = 30 * time.Second 32 | 33 | // TestRunnerStepShutdownTimeout represents the maximum time that the TestRunner 34 | // will wait for all TestSteps to complete after all Targets have reached the end 35 | // of the pipeline. This timeout is only relevant if a cancellation signal is *not* 36 | // delivered. 37 | 38 | // TestRunnerStepShutdownTimeout controls a block of the TestRunner which worksas 39 | // a watchdog, i.e. if there are multiple steps that need to return, the timeout 40 | // is reset every time a step returns. The timeout should be handled so that it 41 | // doesn't reset when a TestStep returns. 42 | var TestRunnerStepShutdownTimeout = 5 * time.Second 43 | 44 | // DefaultTargetLockDuration is the default value for -targetLockDuration. 45 | // It is the amount of time target lock is extended by while the job is running. 46 | const DefaultTargetLockDuration = 10 * time.Minute 47 | -------------------------------------------------------------------------------- /pkg/event/common.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package event 7 | 8 | import ( 9 | "fmt" 10 | "regexp" 11 | 12 | "github.com/linuxboot/contest/pkg/storage/limits" 13 | ) 14 | 15 | // AllowedEventFormat defines the allowed format for an event 16 | var AllowedEventFormat = regexp.MustCompile(`^[a-zA-Z]+$`) 17 | 18 | // Name is a custom type which represents the name of an event 19 | type Name string 20 | 21 | // StateEventName is the name of the event used to emit state information 22 | var StateEventName = Name("TestState") 23 | 24 | // Validate validates that the event name conforms with the framework API 25 | func (e Name) Validate() error { 26 | matched := AllowedEventFormat.MatchString(string(e)) 27 | if !matched { 28 | return fmt.Errorf("event name %s does not comply with events api (does not match %s)", AllowedEventFormat.String(), string(e)) 29 | } 30 | if err := limits.NewValidator().ValidateEventName(string(e)); err != nil { 31 | return err 32 | } 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/event/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package event 7 | 8 | import ( 9 | "fmt" 10 | ) 11 | 12 | // ErrQueryFieldIsAlreadySet is returned when QueryFields failed validation due 13 | // to multiple QueryField which modifies the same field (this is unexpected 14 | // and forbidden). 15 | type ErrQueryFieldIsAlreadySet struct { 16 | FieldValue interface{} 17 | QueryField QueryField 18 | } 19 | 20 | func (err ErrQueryFieldIsAlreadySet) Error() string { 21 | return fmt.Sprintf("field %T is set multiple times: cur_value:%v new_value:%v", 22 | err.QueryField, err.FieldValue, err.QueryField) 23 | } 24 | 25 | // ErrQueryFieldHasZeroValue is returned when a QueryFields failed validation 26 | // due to a QueryField with a zero value (this is unexpected and forbidden). 27 | type ErrQueryFieldHasZeroValue struct { 28 | QueryField QueryField 29 | } 30 | 31 | func (err ErrQueryFieldHasZeroValue) Error() string { 32 | return fmt.Sprintf("field %T has a zero value", err.QueryField) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/event/internal/querytools/apply_query_field.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package querytools 7 | 8 | import ( 9 | "reflect" 10 | 11 | "github.com/linuxboot/contest/pkg/event" 12 | "github.com/linuxboot/contest/pkg/event/internal/reflecttools" 13 | ) 14 | 15 | func ApplyQueryField(fieldPtr interface{}, queryField event.QueryField) error { 16 | if reflecttools.IsZero(queryField) { 17 | return event.ErrQueryFieldHasZeroValue{QueryField: queryField} 18 | } 19 | field := reflect.ValueOf(fieldPtr).Elem() 20 | if !reflecttools.IsZero(field.Interface()) { 21 | return event.ErrQueryFieldIsAlreadySet{FieldValue: field.Interface(), QueryField: queryField} 22 | } 23 | field.Set(reflect.ValueOf(queryField).Convert(field.Type())) 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/event/internal/reflecttools/is_zero.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package reflecttools 7 | 8 | import ( 9 | "reflect" 10 | ) 11 | 12 | // IsZero checks if a value behind an interface equals to zero value of its type 13 | func IsZero(v interface{}) bool { 14 | return reflect.ValueOf(v).IsZero() 15 | } 16 | -------------------------------------------------------------------------------- /pkg/event/query.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package event 7 | 8 | import ( 9 | "time" 10 | 11 | "github.com/linuxboot/contest/pkg/types" 12 | ) 13 | 14 | // Query wraps information that are used to build event queries for all type of event objects 15 | type Query struct { 16 | JobID types.JobID 17 | EventNames []Name 18 | EmittedStartTime time.Time 19 | EmittedEndTime time.Time 20 | } 21 | 22 | type QueryField interface{} 23 | -------------------------------------------------------------------------------- /pkg/event/testevent/test_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package testevent_test 7 | 8 | import ( 9 | "errors" 10 | "testing" 11 | "time" 12 | 13 | "github.com/linuxboot/contest/pkg/event" 14 | "github.com/stretchr/testify/assert" 15 | 16 | . "github.com/linuxboot/contest/pkg/event/testevent" 17 | ) 18 | 19 | func TestBuildQuery_Positive(t *testing.T) { 20 | _, err := QueryFields{ 21 | QueryJobID(1), 22 | QueryEmittedStartTime(time.Now()), 23 | QueryEmittedEndTime(time.Now()), 24 | }.BuildQuery() 25 | assert.NoError(t, err) 26 | } 27 | 28 | func TestBuildQuery_NoDups(t *testing.T) { 29 | _, err := QueryFields{ 30 | QueryJobID(2), 31 | QueryEmittedStartTime(time.Now()), 32 | QueryEmittedStartTime(time.Now()), 33 | }.BuildQuery() 34 | assert.Error(t, err) 35 | assert.True(t, errors.As(err, &event.ErrQueryFieldIsAlreadySet{})) 36 | } 37 | 38 | func TestBuildQuery_NoZeroValues(t *testing.T) { 39 | _, err := QueryFields{ 40 | QueryJobID(0), 41 | QueryEmittedStartTime(time.Now()), 42 | QueryEmittedEndTime(time.Now()), 43 | }.BuildQuery() 44 | assert.Error(t, err) 45 | assert.True(t, errors.As(err, &event.ErrQueryFieldHasZeroValue{})) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/job/events_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package job 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/linuxboot/contest/pkg/event" 14 | ) 15 | 16 | func TestEventToJobStateMapping(t *testing.T) { 17 | m := map[State]event.Name{} 18 | for _, e := range JobStateEvents { 19 | st, err := EventNameToJobState(e) 20 | require.NoError(t, err) 21 | m[st] = e 22 | } 23 | require.Equal(t, 8, len(m)) 24 | st, err := EventNameToJobState(event.Name("foo")) 25 | require.Error(t, err) 26 | require.Equal(t, JobStateUnknown, st) 27 | for k, v := range m { 28 | require.Equal(t, k.String(), string(v)) 29 | } 30 | require.Equal(t, JobStateUnknown.String(), "JobStateUnknown") 31 | } 32 | -------------------------------------------------------------------------------- /pkg/job/job_test.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var ( 11 | currentDescriptorVersion = CurrentDescriptorVersion() 12 | ) 13 | 14 | type Case struct { 15 | version string 16 | expectedErrMsg string 17 | } 18 | 19 | func TestValidVersion(t *testing.T) { 20 | valid := Descriptor{ 21 | Version: currentDescriptorVersion, 22 | } 23 | 24 | require.NoError(t, valid.CheckVersion()) 25 | } 26 | 27 | func TestEmptyVersion(t *testing.T) { 28 | emptyVersionJD := Descriptor{ 29 | Version: "", 30 | } 31 | 32 | require.EqualError( 33 | t, 34 | emptyVersionJD.CheckVersion(), 35 | "version Error: Empty Job Descriptor Version Field", 36 | ) 37 | } 38 | 39 | func TestIncompatibleVersions(t *testing.T) { 40 | var cases []Case 41 | 42 | cases = append(cases, Case{ 43 | fmt.Sprintf("%d.%d", JobDescriptorMajorVersion, JobDescriptorMinorVersion+1), 44 | "version Error: The Job Descriptor Version %s is not compatible with the server: %s", 45 | }) 46 | 47 | cases = append(cases, Case{ 48 | fmt.Sprintf("%d.%d", JobDescriptorMajorVersion+1, JobDescriptorMinorVersion), 49 | "version Error: The Job Descriptor Version %s is not compatible with the server: %s", 50 | }) 51 | 52 | for _, c := range cases { 53 | require.EqualError( 54 | t, 55 | (&Descriptor{Version: c.version}).CheckVersion(), 56 | fmt.Sprintf(c.expectedErrMsg, c.version, currentDescriptorVersion), 57 | ) 58 | } 59 | 60 | } 61 | 62 | func TestInvalidVersion(t *testing.T) { 63 | cases := []Case{ 64 | {"1.", "version Error: strconv.Atoi: parsing \"\": invalid syntax"}, 65 | {".0", "version Error: strconv.Atoi: parsing \"\": invalid syntax"}, 66 | {".", "version Error: strconv.Atoi: parsing \"\": invalid syntax"}, 67 | {"1.a", "version Error: strconv.Atoi: parsing \"a\": invalid syntax"}, 68 | {"a.0", "version Error: strconv.Atoi: parsing \"a\": invalid syntax"}, 69 | {"123", "version Error: Incorrect Job Descriptor Version 123"}, 70 | {"abc", "version Error: Incorrect Job Descriptor Version abc"}, 71 | } 72 | 73 | for _, c := range cases { 74 | require.EqualError( 75 | t, 76 | (&Descriptor{Version: c.version}).CheckVersion(), 77 | c.expectedErrMsg, 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/job/request.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package job 7 | 8 | import ( 9 | "time" 10 | 11 | "github.com/linuxboot/contest/pkg/types" 12 | ) 13 | 14 | // Request represents an incoming Job request which should be persisted in storage 15 | type Request struct { 16 | JobID types.JobID 17 | JobName string 18 | Requestor string 19 | ServerID string 20 | RequestTime time.Time 21 | 22 | // JobDescriptor represents the raw descriptor as submitted by the user. 23 | JobDescriptor string 24 | 25 | // ExtendedDescriptor represents the deserialized job description extended 26 | // with with the full description of the test steps obtained from the test 27 | // fetcher. Since the descriptions of the test steps might not be inlined in 28 | // the descriptor itself, upon receiving a job request, job manager will fetch 29 | // the desciption and build extended descriptor accordingly. The extended 30 | // descriptor is then used upon requesting a resume without having a dependency 31 | // on the test fetcher. 32 | ExtendedDescriptor *ExtendedDescriptor 33 | } 34 | -------------------------------------------------------------------------------- /pkg/job/tags.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package job 7 | 8 | import ( 9 | "fmt" 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | // InternalTagPrefix denotes tags that can only be manipulated by the server. 15 | const InternalTagPrefix = "_" 16 | 17 | var validTagRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) 18 | 19 | // IsValidTag validates the format of tags, only allowing [a-zA-Z0-9_-]. 20 | func IsValidTag(tag string, allowInternal bool) error { 21 | if !validTagRegex.MatchString(tag) { 22 | return fmt.Errorf("%q is not a valid tag", tag) 23 | } 24 | if !allowInternal && IsInternalTag(tag) { 25 | return fmt.Errorf("%q is an internal tag and is not allowed", tag) 26 | } 27 | return nil 28 | } 29 | 30 | // IsInternalTag returns true if a tag is an internal tag. 31 | func IsInternalTag(tag string) bool { 32 | return strings.HasPrefix(tag, InternalTagPrefix) 33 | } 34 | 35 | // CheckTags validates the format of tags, only allowing [a-zA-Z0-9_-] and checking for dups. 36 | func CheckTags(tags []string, allowInternal bool) error { 37 | tm := make(map[string]bool, len(tags)) 38 | for _, tag := range tags { 39 | if err := IsValidTag(tag, allowInternal); err != nil { 40 | return err 41 | } 42 | if tm[tag] { 43 | return fmt.Errorf("duplicate tag %q", tag) 44 | } 45 | tm[tag] = true 46 | } 47 | return nil 48 | } 49 | 50 | // AddTags adds one ore more tags to tags unless already present. 51 | func AddTags(tags []string, moreTags ...string) []string { 52 | loop: 53 | for _, newTag := range moreTags { 54 | for _, tag := range tags { 55 | if tag == newTag { 56 | continue loop 57 | } 58 | } 59 | tags = append(tags, newTag) 60 | } 61 | return tags 62 | } 63 | -------------------------------------------------------------------------------- /pkg/jobmanager/list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package jobmanager 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/linuxboot/contest/pkg/api" 12 | "github.com/linuxboot/contest/pkg/job" 13 | ) 14 | 15 | func (jm *JobManager) list(ev *api.Event) *api.EventResponse { 16 | ctx := ev.Context 17 | evResp := &api.EventResponse{ 18 | Requestor: ev.Msg.Requestor(), 19 | Err: nil, 20 | } 21 | msg, ok := ev.Msg.(api.EventListMsg) 22 | if !ok { 23 | evResp.Err = fmt.Errorf("invaid argument type %T", ev.Msg) 24 | return evResp 25 | } 26 | jobQuery := msg.Query 27 | if jm.config.instanceTag != "" { 28 | jobQuery.Tags = job.AddTags(jobQuery.Tags, jm.config.instanceTag) 29 | } 30 | res, err := jm.jsm.ListJobs(ctx, jobQuery) 31 | if err != nil { 32 | evResp.Err = fmt.Errorf("failed to list jobs: %w", err) 33 | return evResp 34 | } 35 | evResp.JobIDs = res 36 | return evResp 37 | } 38 | -------------------------------------------------------------------------------- /pkg/jobmanager/options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package jobmanager 7 | 8 | import ( 9 | "time" 10 | 11 | "github.com/benbjohnson/clock" 12 | 13 | "github.com/linuxboot/contest/pkg/api" 14 | configPkg "github.com/linuxboot/contest/pkg/config" 15 | ) 16 | 17 | // Option is an additional argument to method New to change the behavior 18 | // of the JobManager. 19 | type Option interface { 20 | apply(*config) 21 | } 22 | 23 | type config struct { 24 | apiOptions []api.Option 25 | instanceTag string 26 | targetLockDuration time.Duration 27 | clock clock.Clock 28 | } 29 | 30 | // OptionAPI wraps api.Option to implement Option. 31 | type OptionAPI struct { 32 | api.Option 33 | } 34 | 35 | // apply implements Option. 36 | func (opt OptionAPI) apply(config *config) { 37 | config.apiOptions = append(config.apiOptions, opt.Option) 38 | } 39 | 40 | // APIOption is a syntax-sugar function which just wraps an api.Option 41 | // into OptionAPI. 42 | func APIOption(option api.Option) Option { 43 | return OptionAPI{Option: option} 44 | } 45 | 46 | // OptionInstanceTag wraps a string to be used as instance tag. 47 | type OptionInstanceTag string 48 | 49 | func (opt OptionInstanceTag) apply(config *config) { 50 | config.instanceTag = string(opt) 51 | } 52 | 53 | // OptionTargetLockDuration wraps time.Duration to be used as an option. 54 | type OptionTargetLockDuration time.Duration 55 | 56 | func (opt OptionTargetLockDuration) apply(config *config) { 57 | config.targetLockDuration = time.Duration(opt) 58 | } 59 | 60 | type optionClock struct { 61 | clock clock.Clock 62 | } 63 | 64 | func (opt optionClock) apply(config *config) { 65 | config.clock = opt.clock 66 | } 67 | 68 | // OptionClock wraps clock.Clock to be used as an option. 69 | func OptionClock(clk clock.Clock) Option { 70 | return optionClock{clock: clk} 71 | } 72 | 73 | // getConfig converts a set of Option-s into one structure "Config". 74 | func getConfig(opts ...Option) config { 75 | result := config{ 76 | targetLockDuration: configPkg.DefaultTargetLockDuration, 77 | clock: clock.New(), 78 | } 79 | for _, opt := range opts { 80 | opt.apply(&result) 81 | } 82 | return result 83 | } 84 | -------------------------------------------------------------------------------- /pkg/jobmanager/retry.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package jobmanager 7 | 8 | import ( 9 | "fmt" 10 | "github.com/linuxboot/contest/pkg/api" 11 | ) 12 | 13 | func (jm *JobManager) retry(ev *api.Event) *api.EventResponse { 14 | return &api.EventResponse{ 15 | Requestor: ev.Msg.Requestor(), 16 | Err: fmt.Errorf("Not implemented"), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/jobmanager/steps.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package jobmanager 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/linuxboot/contest/pkg/job" 12 | "github.com/linuxboot/contest/pkg/pluginregistry" 13 | "github.com/linuxboot/contest/pkg/test" 14 | ) 15 | 16 | // stepsResolver is an interface which determines how to fetch TestStepsDescriptors, which could 17 | // have either already been pre-calculated, or built by the TestFetcher. 18 | type stepsResolver interface { 19 | GetStepsDescriptors(context.Context) ([]test.TestStepsDescriptors, error) 20 | } 21 | 22 | type literalStepsResolver struct { 23 | stepsDescriptors []test.TestStepsDescriptors 24 | } 25 | 26 | func (l literalStepsResolver) GetStepsDescriptors(context.Context) ([]test.TestStepsDescriptors, error) { 27 | return l.stepsDescriptors, nil 28 | } 29 | 30 | type fetcherStepsResolver struct { 31 | jobDescriptor *job.Descriptor 32 | registry *pluginregistry.PluginRegistry 33 | } 34 | 35 | func (f fetcherStepsResolver) GetStepsDescriptors(ctx context.Context) ([]test.TestStepsDescriptors, error) { 36 | var descriptors []test.TestStepsDescriptors 37 | for _, testDescriptor := range f.jobDescriptor.TestDescriptors { 38 | bundleTestFetcher, err := f.registry.NewTestFetcherBundle(ctx, testDescriptor) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | testName, stepDescriptors, err := bundleTestFetcher.TestFetcher.Fetch(ctx, bundleTestFetcher.FetchParameters) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | var cleanupDescriptors []*test.TestStepDescriptor 49 | if bundleTestFetcher.CleanupFetcher != nil { 50 | _, cleanupDescriptors, err = bundleTestFetcher.CleanupFetcher.Fetch(ctx, bundleTestFetcher.CleanupParameters) 51 | if err != nil { 52 | return nil, err 53 | } 54 | } 55 | 56 | descriptors = append( 57 | descriptors, 58 | test.TestStepsDescriptors{ 59 | TestName: testName, 60 | TestSteps: stepDescriptors, 61 | CleanupSteps: cleanupDescriptors, 62 | }, 63 | ) 64 | } 65 | 66 | return descriptors, nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/jobmanager/stop.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package jobmanager 7 | 8 | import ( 9 | "fmt" 10 | "time" 11 | 12 | "github.com/linuxboot/contest/pkg/api" 13 | "github.com/linuxboot/contest/pkg/job" 14 | "github.com/linuxboot/contest/pkg/logging" 15 | ) 16 | 17 | func (jm *JobManager) stop(ev *api.Event) *api.EventResponse { 18 | ctx := ev.Context 19 | msg := ev.Msg.(api.EventStopMsg) 20 | jobID := msg.JobID 21 | // CancelJob is asynchronous, it closes the Job's cancellation signal which 22 | // is propagated all the way down to the TestRunner. TestRunner will wait 23 | // TestRunnerShutdownTimeout before flagging the test as timed out. JobRunner 24 | // will attempt to call Release on TargetManager and will wait up to 25 | // TargetManagerReleaseTimeout for Release to return. 26 | err := jm.CancelJob(jobID) 27 | if err != nil { 28 | logging.Errorf(ctx, "Cannot stop job: %v", err) 29 | return &api.EventResponse{Err: fmt.Errorf("could not stop job: %v", err)} 30 | } 31 | _ = jm.emitEvent(ctx, jobID, job.EventJobCancelling) 32 | return &api.EventResponse{ 33 | JobID: jobID, 34 | Requestor: ev.Msg.Requestor(), 35 | Err: nil, 36 | Status: &job.Status{ 37 | Name: "UnknownJobName", 38 | State: string(job.EventJobCancelling), 39 | StartTime: time.Now(), 40 | }, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/logging/default_options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package logging 7 | 8 | import ( 9 | "context" 10 | stdruntime "runtime" 11 | "strings" 12 | 13 | "github.com/facebookincubator/go-belt/pkg/runtime" 14 | "github.com/facebookincubator/go-belt/tool/logger" 15 | "github.com/facebookincubator/go-belt/tool/logger/implementation/logrus" 16 | "github.com/facebookincubator/go-belt/tool/logger/implementation/logrus/formatter" 17 | loggertypes "github.com/facebookincubator/go-belt/tool/logger/types" 18 | ) 19 | 20 | // DefaultOptions is a set options recommended to use by default. 21 | func WithBelt( 22 | ctx context.Context, 23 | logLevel logger.Level, 24 | ) context.Context { 25 | l := logrus.DefaultLogrusLogger() 26 | l.Formatter = &formatter.CompactText{ 27 | TimestampFormat: "2006-01-02T15:04:05.000Z07:00", 28 | } 29 | ctx = logger.CtxWithLogger(ctx, logrus.New(l, loggertypes.OptionGetCallerFunc(getCallerPC)).WithLevel(logLevel)) 30 | return ctx 31 | } 32 | 33 | func getCallerPC() runtime.PC { 34 | return runtime.Caller(func(pc uintptr) bool { 35 | if !runtime.DefaultCallerPCFilter(pc) { 36 | return false 37 | } 38 | 39 | fn := stdruntime.FuncForPC(pc) 40 | funcName := fn.Name() 41 | switch { 42 | case strings.Contains(funcName, "pkg/logging"): 43 | return false 44 | } 45 | 46 | return true 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/metrics/perf/perf.go: -------------------------------------------------------------------------------- 1 | package perf 2 | 3 | // perf counter keys constants 4 | const ( 5 | ACQUIRED_TARGETS string = "acquired_targets" 6 | RUNNING_JOBS string = "running_jobs" 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/pluginregistry/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package pluginregistry 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/linuxboot/contest/pkg/test" 12 | ) 13 | 14 | type ErrStepLabelIsMandatory struct { 15 | TestStepDescriptor test.TestStepDescriptor 16 | } 17 | 18 | func (err ErrStepLabelIsMandatory) Error() string { 19 | return fmt.Sprintf("step has no label, but it is mandatory (step: %+v)", err.TestStepDescriptor) 20 | } 21 | 22 | // ErrInvalidStepLabelFormat tells that a variable name doesn't fit the variable name format (alphanum + '_') 23 | type ErrInvalidStepLabelFormat struct { 24 | InvalidName string 25 | Err error 26 | } 27 | 28 | func (err ErrInvalidStepLabelFormat) Error() string { 29 | return fmt.Sprintf("'%s' doesn't match variable name format: %v", err.InvalidName, err.Err) 30 | } 31 | 32 | func (err ErrInvalidStepLabelFormat) Unwrap() error { 33 | return err.Err 34 | } 35 | -------------------------------------------------------------------------------- /pkg/pluginregistry/pluginregistry_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package pluginregistry 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "testing" 12 | 13 | "github.com/linuxboot/contest/pkg/event" 14 | "github.com/linuxboot/contest/pkg/event/testevent" 15 | "github.com/linuxboot/contest/pkg/test" 16 | 17 | "github.com/facebookincubator/go-belt/beltctx" 18 | "github.com/facebookincubator/go-belt/tool/logger" 19 | "github.com/facebookincubator/go-belt/tool/logger/implementation/logrus" 20 | 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | // Definition of two dummy TestSteps to be used to test the PluginRegistry 25 | 26 | // AStep implements a dummy TestStep 27 | type AStep struct{} 28 | 29 | // NewAStep initializes a new AStep 30 | func NewAStep() test.TestStep { 31 | return &AStep{} 32 | } 33 | 34 | // ValidateParameters validates the parameters for the AStep 35 | func (e AStep) ValidateParameters(ctx context.Context, params test.TestStepParameters) error { 36 | return nil 37 | } 38 | 39 | // Name returns the name of the AStep 40 | func (e AStep) Name() string { 41 | return "AStep" 42 | } 43 | 44 | // Run executes the AStep 45 | func (e AStep) Run( 46 | ctx context.Context, 47 | ch test.TestStepChannels, 48 | ev testevent.Emitter, 49 | stepsVars test.StepsVariables, 50 | params test.TestStepParameters, 51 | resumeState json.RawMessage, 52 | ) (json.RawMessage, error) { 53 | return nil, nil 54 | } 55 | 56 | func TestRegisterTestStep(t *testing.T) { 57 | ctx, cancel := context.WithCancel(logger.CtxWithLogger(context.Background(), logrus.Default())) 58 | defer beltctx.Flush(ctx) 59 | defer cancel() 60 | pr := NewPluginRegistry(ctx) 61 | err := pr.RegisterTestStep("AStep", NewAStep, []event.Name{"AStepEventName"}) 62 | require.NoError(t, err) 63 | } 64 | 65 | func TestRegisterTestStepDoesNotValidate(t *testing.T) { 66 | ctx, cancel := context.WithCancel(logger.CtxWithLogger(context.Background(), logrus.Default())) 67 | defer beltctx.Flush(ctx) 68 | defer cancel() 69 | pr := NewPluginRegistry(ctx) 70 | err := pr.RegisterTestStep("AStep", NewAStep, []event.Name{"Event which does not validate"}) 71 | require.Error(t, err) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/remote/proto.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package remote 7 | 8 | import ( 9 | "bufio" 10 | "encoding/json" 11 | "fmt" 12 | "io" 13 | ) 14 | 15 | type StartMessage struct { 16 | SessionID string `json:"sid"` 17 | } 18 | 19 | type PollMessage struct { 20 | Stdout string `json:"stdout,omitempty"` 21 | Stderr string `json:"stderr,omitempty"` 22 | 23 | // ExitCode is non-nil when the controlled process exited 24 | ExitCode *int `json:"exitcode"` 25 | 26 | // Error is any error encountered while trying to reach the agent 27 | Error string `json:"error,omitempty"` 28 | } 29 | 30 | // SendResponse conveys to the caller a given response object o 31 | func SendResponse(o interface{}) error { 32 | bytes, err := json.Marshal(o) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | _, err = fmt.Printf("%s\n", string(bytes)) 38 | return err 39 | } 40 | 41 | // RecvResponse reads a response object from the given reader 42 | func RecvResponse(r io.Reader, o interface{}) error { 43 | s := bufio.NewScanner(r) 44 | if !s.Scan() { 45 | return fmt.Errorf("no input") 46 | } 47 | if s.Err() != nil { 48 | return s.Err() 49 | } 50 | 51 | return json.Unmarshal(s.Bytes(), o) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/remote/proto_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package remote 7 | 8 | import ( 9 | "bytes" 10 | "encoding/json" 11 | "io" 12 | "os" 13 | "testing" 14 | 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestSendResponseSimple(t *testing.T) { 19 | prev := os.Stdout 20 | defer func() { 21 | os.Stdout = prev 22 | }() 23 | 24 | r, w, _ := os.Pipe() 25 | os.Stdout = w 26 | 27 | result := make(chan []byte) 28 | go func() { 29 | var buf bytes.Buffer 30 | _, _ = io.Copy(&buf, r) 31 | result <- buf.Bytes() 32 | }() 33 | 34 | sid := "42" 35 | err := SendResponse(&StartMessage{sid}) 36 | require.NoError(t, err) 37 | 38 | // done with capturing stdout 39 | w.Close() 40 | 41 | var recv StartMessage 42 | err = json.Unmarshal(<-result, &recv) 43 | require.NoError(t, err) 44 | 45 | require.Equal(t, sid, recv.SessionID) 46 | } 47 | 48 | func TestRecvResponseSimple(t *testing.T) { 49 | msg := StartMessage{"42"} 50 | data, err := json.Marshal(&msg) 51 | require.NoError(t, err) 52 | 53 | var recv StartMessage 54 | err = RecvResponse(bytes.NewReader(data), &recv) 55 | require.NoError(t, err) 56 | 57 | require.Equal(t, msg, recv) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/remote/sync.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package remote 7 | 8 | import ( 9 | "bytes" 10 | "io" 11 | "sync" 12 | ) 13 | 14 | // SafeSignal is a goroutine safe signalling mechanism 15 | // It can have multiple goroutines that trigger the signal without 16 | // interfering with eachother (or crashing as using a channel would) 17 | // Currently designed for a single waiter goroutine. 18 | type SafeSignal struct { 19 | sync.Mutex 20 | done bool 21 | c *sync.Cond 22 | } 23 | 24 | func newSafeSignal() *SafeSignal { 25 | s := &SafeSignal{} 26 | s.c = &sync.Cond{L: s} 27 | return s 28 | } 29 | 30 | func (s *SafeSignal) Signal() { 31 | s.Lock() 32 | s.done = true 33 | s.Unlock() 34 | 35 | s.c.Signal() 36 | } 37 | 38 | func (s *SafeSignal) Wait() { 39 | s.Lock() 40 | defer s.Unlock() 41 | 42 | for !s.done { 43 | s.c.Wait() 44 | } 45 | } 46 | 47 | type SafeBuffer struct { 48 | b bytes.Buffer 49 | mu sync.Mutex 50 | } 51 | 52 | func (sb *SafeBuffer) Write(data []byte) (int, error) { 53 | sb.mu.Lock() 54 | defer sb.mu.Unlock() 55 | 56 | return sb.b.Write(data) 57 | } 58 | 59 | func (sb *SafeBuffer) Read(data []byte) (int, error) { 60 | sb.mu.Lock() 61 | defer sb.mu.Unlock() 62 | 63 | return sb.b.Read(data) 64 | } 65 | 66 | func (sb *SafeBuffer) Len() int { 67 | sb.mu.Lock() 68 | defer sb.mu.Unlock() 69 | 70 | return sb.b.Len() 71 | } 72 | 73 | type LenReader interface { 74 | io.Reader 75 | Len() int 76 | } 77 | 78 | // SafeExitCode is a sync storage for the exit code of a process 79 | // the initial value of nil cannot be reset after the first set 80 | type SafeExitCode struct { 81 | exitCode *int 82 | 83 | mu sync.Mutex 84 | } 85 | 86 | func (s *SafeExitCode) Store(code int) { 87 | s.mu.Lock() 88 | defer s.mu.Unlock() 89 | 90 | s.exitCode = &code 91 | } 92 | 93 | func (s *SafeExitCode) Load() *int { 94 | s.mu.Lock() 95 | defer s.mu.Unlock() 96 | 97 | return s.exitCode 98 | } 99 | -------------------------------------------------------------------------------- /pkg/remote/sync_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package remote 7 | 8 | import ( 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestSafeSignalSimple(t *testing.T) { 16 | s := newSafeSignal() 17 | c := make(chan struct{}) 18 | 19 | go func() { 20 | s.Wait() 21 | close(c) 22 | }() 23 | 24 | s.Signal() 25 | 26 | select { 27 | case <-c: 28 | return 29 | 30 | case <-time.After(time.Second): 31 | t.Fail() 32 | } 33 | } 34 | 35 | func TestSafeSignalMultipleSignal(t *testing.T) { 36 | s := newSafeSignal() 37 | 38 | // should not panic on multiple signal calls 39 | defer func() { 40 | if err := recover(); err != nil { 41 | t.Fail() 42 | } 43 | }() 44 | 45 | s.Signal() 46 | s.Signal() 47 | } 48 | 49 | func TestSafeBufferSimple(t *testing.T) { 50 | var b SafeBuffer 51 | require.Equal(t, 0, b.Len()) 52 | 53 | data := []byte{1, 2, 3} 54 | n, err := b.Write(data) 55 | 56 | require.NoError(t, err) 57 | require.Equal(t, len(data), n) 58 | 59 | read := make([]byte, len(data)) 60 | n, err = b.Read(read) 61 | 62 | require.NoError(t, err) 63 | require.Equal(t, len(data), n) 64 | require.Equal(t, read, data) 65 | } 66 | 67 | func TestSafeExitCodeSimple(t *testing.T) { 68 | var s SafeExitCode 69 | require.Equal(t, (*int)(nil), s.Load()) 70 | 71 | s.Store(42) 72 | require.Equal(t, 42, *s.Load()) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/runner/event.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package runner 7 | 8 | import ( 9 | "github.com/linuxboot/contest/pkg/event" 10 | "github.com/linuxboot/contest/pkg/types" 11 | ) 12 | 13 | // RunStartedPayload represents the payload carried by a failure event (e.g. JobStateFailed, JobStateCancelled, etc.) 14 | type RunStartedPayload struct { 15 | RunID types.RunID 16 | } 17 | 18 | // EventRunStarted indicates that a run has begun 19 | var EventRunStarted = event.Name("RunStarted") 20 | 21 | // EventTestError indicates that a test failed. 22 | var EventTestError = event.Name("TestError") 23 | 24 | // EventVariableEmitted is emitted each time when a step adds variable 25 | var EventVariableEmitted = event.Name("VariableEmitted") 26 | -------------------------------------------------------------------------------- /pkg/signaling/signaler.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | type ctxKey[T Signal] struct { 10 | Signal T 11 | } 12 | 13 | // Signal is an interface defining constraints of a type which could be used 14 | // as a signal (in functions WithSignal, Until and IsSignaledWith). 15 | type Signal interface { 16 | error 17 | comparable 18 | } 19 | 20 | type signaler chan struct{} 21 | 22 | func getSignalChan[T Signal](ctx context.Context, signal T) signaler { 23 | signaler, _ := ctx.Value(ctxKey[T]{Signal: signal}).(signaler) 24 | // If a signaller is not defined then a nil is returned, 25 | // and a nil channel is an infinitely open channel without events, 26 | // so it blocks reading forever. 27 | return signaler 28 | } 29 | 30 | func withSignalChan[T Signal](ctx context.Context, signal T) (context.Context, signaler) { 31 | s := make(signaler) 32 | return context.WithValue(ctx, ctxKey[T]{Signal: signal}, s), s 33 | } 34 | 35 | // IsSignaledWith returns true if the context received the signal. 36 | func IsSignaledWith[T Signal](ctx context.Context, signal T) (bool, error) { 37 | return chanIsClosed(getSignalChan(ctx, signal)) 38 | } 39 | 40 | func chanIsClosed(c <-chan struct{}) (bool, error) { 41 | select { 42 | case _, isOpen := <-c: 43 | if isOpen { 44 | // Any signaling here supposed to be broadcast, and the only broadcast 45 | // kind of event on a channel is a closure. So no events should be sent 46 | // through this channel, but it could be closed. 47 | return false, fmt.Errorf("the channel has an event, but is not closed; this was supposed to be impossible") 48 | } 49 | return true, nil 50 | default: 51 | return false, nil 52 | } 53 | } 54 | 55 | // Until works similar to Done(), but waits for a specific signal 56 | // defined via WithSignal function. 57 | func Until[T Signal](ctx context.Context, signal T) <-chan struct{} { 58 | return getSignalChan(ctx, signal) 59 | } 60 | 61 | // SignalFunc is similar to context.CancelFunc, but instead of closing 62 | // the context it sends a signal through it (which could be observed with Until 63 | // and IsSignaledWith). 64 | type SignalFunc func() 65 | 66 | var _ = context.CancelFunc((SignalFunc)(nil)) // check if CancelFunc and SignalFunc has the same signature 67 | 68 | // WithSignal is similar to context.WithCancel, but the returned function 69 | // sends signal through the context (which could be observed with Until 70 | // and IsSignaledWith). 71 | func WithSignal[T Signal](ctx context.Context, signal T) (context.Context, SignalFunc) { 72 | ctx, s := withSignalChan(ctx, signal) 73 | var closeOnce sync.Once 74 | return ctx, func() { 75 | closeOnce.Do(func() { 76 | close(s) 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pkg/signaling/signaler_flaky_test.go: -------------------------------------------------------------------------------- 1 | //go:build flakytests 2 | 3 | package signaling 4 | 5 | import ( 6 | "context" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestGC(t *testing.T) { 14 | // parent 15 | ctx := context.Background() 16 | ctx, _ = WithSignal(ctx, unitTestCustomSignal0) 17 | 18 | gc := func() { 19 | runtime.GC() 20 | runtime.Gosched() 21 | runtime.GC() 22 | runtime.Gosched() 23 | } 24 | 25 | // mem stats before the child 26 | var ( 27 | memStatsBefore runtime.MemStats 28 | memStatsAfter runtime.MemStats 29 | ) 30 | gc() 31 | runtime.ReadMemStats(&memStatsBefore) 32 | 33 | // short-living child 34 | WithSignal(ctx, unitTestCustomSignal1) 35 | 36 | // mem stats after the child 37 | gc() 38 | runtime.ReadMemStats(&memStatsAfter) 39 | 40 | require.Equal(t, memStatsBefore.HeapInuse, memStatsAfter.HeapInuse) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/signals/paused.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | type pausedType struct{} 4 | 5 | func (pausedType) Error() string { return "paused" } 6 | 7 | // Paused is a signal (signaling.Signal) to notify goroutines 8 | // that the process is trying to pause and all long-running 9 | // routines should save their state (if required) and return. 10 | var Paused = pausedType{} 11 | -------------------------------------------------------------------------------- /pkg/storage/job_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package storage 7 | 8 | import ( 9 | "context" 10 | "testing" 11 | 12 | "github.com/facebookincubator/go-belt/tool/logger" 13 | "github.com/linuxboot/contest/pkg/logging" 14 | "github.com/linuxboot/contest/pkg/types" 15 | 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | type testJobStorageManagerFixture struct { 20 | ctx context.Context 21 | jobID types.JobID 22 | jobQuery *JobQuery 23 | } 24 | 25 | func mockJobStorageManagerData() *testJobStorageManagerFixture { 26 | query, _ := BuildJobQuery() 27 | 28 | return &testJobStorageManagerFixture{ 29 | ctx: logging.WithBelt(context.Background(), logger.LevelDebug), 30 | jobID: types.JobID(0), 31 | jobQuery: query, 32 | } 33 | } 34 | 35 | func TestJobStorageConsistency(t *testing.T) { 36 | f := mockJobStorageManagerData() 37 | vault := NewSimpleEngineVault() 38 | jsm := NewJobStorageManager(vault) 39 | 40 | var cases = []struct { 41 | name string 42 | getter func(ctx context.Context, jsm *JobStorageManager) 43 | }{ 44 | { 45 | "TestGetJobRequest", 46 | func(ctx context.Context, jsm *JobStorageManager) { _, _ = jsm.GetJobRequest(ctx, f.jobID) }, 47 | }, 48 | { 49 | "TestGetJobReport", 50 | func(ctx context.Context, jsm *JobStorageManager) { _, _ = jsm.GetJobReport(ctx, f.jobID) }, 51 | }, 52 | { 53 | "TestListJobs", 54 | func(ctx context.Context, jsm *JobStorageManager) { _, _ = jsm.ListJobs(ctx, f.jobQuery) }, 55 | }, 56 | } 57 | 58 | for _, tc := range cases { 59 | t.Run(tc.name, func(t *testing.T) { 60 | var storage, storageAsync *nullStorage 61 | storage, storageAsync = mockStorage(t, vault) 62 | 63 | // test with default context 64 | tc.getter(f.ctx, &jsm) 65 | require.Equal(t, storage.GetJobRequestCount(), 1) 66 | require.Equal(t, storageAsync.GetJobRequestCount(), 0) 67 | 68 | // test with explicit strong consistency 69 | ctx := WithConsistencyModel(f.ctx, ConsistentReadAfterWrite) 70 | tc.getter(ctx, &jsm) 71 | require.Equal(t, storage.GetJobRequestCount(), 2) 72 | require.Equal(t, storageAsync.GetJobRequestCount(), 0) 73 | 74 | // test with explicit relaxed consistency 75 | ctx = WithConsistencyModel(ctx, ConsistentEventually) 76 | tc.getter(ctx, &jsm) 77 | require.Equal(t, storage.GetJobRequestCount(), 2) 78 | require.Equal(t, storageAsync.GetJobRequestCount(), 1) 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/storage/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package storage 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/linuxboot/contest/pkg/logging" 12 | ) 13 | 14 | // ConsistencyModel hints at whether queries should go to the primary database 15 | // or any available replica (in which case, the guarantee is eventual consistency) 16 | type ConsistencyModel int 17 | 18 | const ( 19 | ConsistentReadAfterWrite ConsistencyModel = iota 20 | ConsistentEventually 21 | ) 22 | 23 | type consistencyModelKeyT string 24 | 25 | const consistencyModelKey = consistencyModelKeyT("storage_consistency_model") 26 | 27 | // Storage defines the interface that storage engines must implement 28 | type Storage interface { 29 | JobStorage 30 | EventStorage 31 | 32 | // Close flushes and releases resources associated with the storage engine. 33 | Close() error 34 | 35 | // Version returns the version of the storage being used 36 | Version() (uint64, error) 37 | } 38 | 39 | // TransactionalStorage is implemented by storage backends that support transactions. 40 | // Only default isolation level is supported. 41 | type TransactionalStorage interface { 42 | Storage 43 | BeginTx() (TransactionalStorage, error) 44 | Commit() error 45 | Rollback() error 46 | } 47 | 48 | // ResettableStorage is implemented by storage engines that support reset operation 49 | type ResettableStorage interface { 50 | Storage 51 | Reset() error 52 | } 53 | 54 | func isStronglyConsistent(ctx context.Context) bool { 55 | value := ctx.Value(consistencyModelKey) 56 | logging.Debugf(ctx, "consistency model check: %v", value) 57 | 58 | switch model := value.(type) { 59 | case ConsistencyModel: 60 | return model == ConsistentReadAfterWrite 61 | 62 | default: 63 | return true 64 | } 65 | } 66 | 67 | func WithConsistencyModel(ctx context.Context, model ConsistencyModel) context.Context { 68 | return context.WithValue(ctx, consistencyModelKey, model) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/storage/vault.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // storage emitterVault is used to define the storage engines used by ConTest. 7 | // Engines can be overridden via the exported function StoreEngine. 8 | 9 | package storage 10 | 11 | import ( 12 | "fmt" 13 | "github.com/linuxboot/contest/pkg/config" 14 | ) 15 | 16 | type EngineType uint32 17 | 18 | const ( 19 | UnknownEngine EngineType = iota 20 | SyncEngine 21 | AsyncEngine 22 | ) 23 | 24 | type EngineVault interface { 25 | // GetEngine - fetch the engine of selected type from the emitterVault 26 | GetEngine(EngineType) (Storage, error) 27 | } 28 | 29 | type EngineVaultMap map[EngineType]Storage 30 | 31 | type SimpleEngineVault struct { 32 | vault EngineVaultMap 33 | } 34 | 35 | // GetEngine - get storage engine from the vault. Defaults to SyncEngine. 36 | func (v *SimpleEngineVault) GetEngine(engineType EngineType) (Storage, error) { 37 | var found bool 38 | var err error 39 | var engine Storage 40 | if engine, found = v.vault[engineType]; !found { 41 | err = fmt.Errorf("storage #{engineType} not assigned") 42 | } 43 | return engine, err 44 | } 45 | 46 | // StoreEngine - store supplied engine in the emitterVault. As SyncEngine by default 47 | // Switching to a new storage engine implies garbage collecting the old one, 48 | // with possible loss of pending events if not flushed correctly 49 | func (v *SimpleEngineVault) StoreEngine(storageEngine Storage, engineType EngineType) error { 50 | var err error 51 | if storageEngine != nil { 52 | var ver uint64 53 | if ver, err = storageEngine.Version(); err != nil { 54 | err = fmt.Errorf("could not determine storage version: %w", err) 55 | return err 56 | } 57 | 58 | if ver < config.MinStorageVersion { 59 | err = fmt.Errorf("could not configure storage of type %T (minimum storage version: %d, current storage version: %d)", storageEngine, config.MinStorageVersion, ver) 60 | return err 61 | } 62 | 63 | v.vault[engineType] = storageEngine 64 | } else { 65 | delete(v.vault, engineType) 66 | } 67 | 68 | return err 69 | } 70 | 71 | // NewSimpleEngineVault - returns a new instance of SimpleEngineVault 72 | func NewSimpleEngineVault() *SimpleEngineVault { 73 | return &SimpleEngineVault{vault: make(EngineVaultMap)} 74 | } 75 | -------------------------------------------------------------------------------- /pkg/storage/vault_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package storage 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | type singleSilentEngineVaultProvider struct { 15 | singleEngine Storage 16 | } 17 | 18 | func (v *singleSilentEngineVaultProvider) Init() { 19 | v.singleEngine = &nullStorage{} 20 | } 21 | 22 | func (v *singleSilentEngineVaultProvider) Clear() { 23 | v.singleEngine = nil 24 | } 25 | 26 | func (v *singleSilentEngineVaultProvider) GetEngine(_ EngineType) (Storage, error) { 27 | return v.singleEngine, nil 28 | } 29 | 30 | func (v *singleSilentEngineVaultProvider) StoreEngine(storageEngine Storage, engineType EngineType) error { 31 | return nil 32 | } 33 | 34 | func TestStorageEngineVault(t *testing.T) { 35 | const engineId = AsyncEngine 36 | 37 | vault := NewSimpleEngineVault() 38 | 39 | // Nothing here upon creation 40 | engine, err := vault.GetEngine(engineId) 41 | require.Nil(t, engine) 42 | require.Error(t, err) 43 | 44 | // Create engine 45 | require.NoError(t, vault.StoreEngine(&nullStorage{}, engineId)) 46 | engine, err = vault.GetEngine(engineId) 47 | require.NotNil(t, engine) 48 | require.NoError(t, err) 49 | 50 | // Delete engine 51 | require.NoError(t, vault.StoreEngine(nil, engineId)) 52 | engine, err = vault.GetEngine(engineId) 53 | require.Nil(t, engine) 54 | require.Error(t, err) 55 | } 56 | 57 | func TestStorageEngineVaultClearing(t *testing.T) { 58 | vault := NewSimpleEngineVault() 59 | 60 | types := []EngineType{SyncEngine, AsyncEngine} 61 | 62 | for _, engineType := range types { 63 | require.NoError(t, vault.StoreEngine(&nullStorage{}, engineType)) 64 | engine, err := vault.GetEngine(engineType) 65 | require.NotNil(t, engine) 66 | require.NoError(t, err) 67 | } 68 | 69 | vault = NewSimpleEngineVault() 70 | for _, engineType := range types { 71 | engine, err := vault.GetEngine(engineType) 72 | require.Nil(t, engine) 73 | require.Error(t, err) 74 | } 75 | } 76 | 77 | func TestCustomEngineVaultProvider(t *testing.T) { 78 | vault := &singleSilentEngineVaultProvider{} 79 | vault.Init() 80 | 81 | // Now there should be an engine 82 | engine, err := vault.GetEngine(SyncEngine) 83 | require.NotNil(t, engine) 84 | require.NoError(t, err) 85 | 86 | vault.Clear() 87 | // Now there should be no engine, but no error also 88 | engine, err = vault.GetEngine(SyncEngine) 89 | require.Nil(t, engine) 90 | require.NoError(t, err) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/target/target_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package target 7 | 8 | import ( 9 | "encoding/json" 10 | "net" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestNilTarget(t *testing.T) { 17 | var recoverResult interface{} 18 | func() { 19 | defer func() { 20 | recoverResult = recover() 21 | }() 22 | _ = (*Target)(nil).String() 23 | }() 24 | require.Nil(t, recoverResult) 25 | } 26 | 27 | func TestTargetStringification(t *testing.T) { 28 | var t0 *Target 29 | require.Equal(t, `(*Target)(nil)`, t0.String()) 30 | 31 | t1 := &Target{ID: "123"} 32 | require.Equal(t, `Target{ID: "123"}`, t1.String()) 33 | tj1, _ := json.Marshal(t1) 34 | require.Equal(t, `{"ID":"123"}`, string(tj1)) 35 | 36 | t2 := &Target{FQDN: "example.com"} 37 | require.Equal(t, `Target{ID: "", FQDN: "example.com"}`, t2.String()) 38 | tj2, _ := json.Marshal(t2) 39 | require.Equal(t, `{"ID":"","FQDN":"example.com"}`, string(tj2)) 40 | 41 | t3 := &Target{ID: "123", FQDN: "example.com", PrimaryIPv4: net.IPv4(1, 2, 3, 4)} 42 | require.Equal(t, `Target{ID: "123", FQDN: "example.com", PrimaryIPv4: "1.2.3.4"}`, t3.String()) 43 | tj3, _ := json.Marshal(t3) 44 | require.Equal(t, `{"ID":"123","FQDN":"example.com","PrimaryIPv4":"1.2.3.4"}`, string(tj3)) 45 | 46 | t4 := &Target{ID: "123", FQDN: "example.com", PrimaryIPv6: net.IPv6loopback} 47 | require.Equal(t, `Target{ID: "123", FQDN: "example.com", PrimaryIPv6: "::1"}`, t4.String()) 48 | tj4, _ := json.Marshal(t4) 49 | require.Equal(t, `{"ID":"123","FQDN":"example.com","PrimaryIPv6":"::1"}`, string(tj4)) 50 | 51 | t5 := &Target{ID: "123", TargetManagerState: json.RawMessage([]byte(`{"hello": "world"}`))} 52 | require.Equal(t, `Target{ID: "123", TMS: "{"hello": "world"}"}`, t5.String()) 53 | tj5, _ := json.Marshal(t5) 54 | require.Equal(t, `{"ID":"123","TMS":{"hello":"world"}}`, string(tj5)) 55 | } 56 | 57 | func TestErrPayloadMarshalling(t *testing.T) { 58 | t.Run("Nil", func(t *testing.T) { 59 | res, err := UnmarshalErrPayload(nil) 60 | require.NoError(t, err) 61 | require.Equal(t, &ErrPayload{}, res) 62 | }) 63 | 64 | t.Run("null_bytes", func(t *testing.T) { 65 | res, err := UnmarshalErrPayload([]byte("null")) 66 | require.NoError(t, err) 67 | require.Nil(t, res) 68 | }) 69 | 70 | t.Run("MarshalUnmarshal", func(t *testing.T) { 71 | payload, err := MarshallErrPayload("dummy") 72 | require.NoError(t, err) 73 | res, err := UnmarshalErrPayload(payload) 74 | require.NoError(t, err) 75 | require.Equal(t, ErrPayload{Error: "dummy"}, *res) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /pkg/target/targetmanager.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package target 7 | 8 | import ( 9 | "context" 10 | "time" 11 | 12 | "github.com/linuxboot/contest/pkg/types" 13 | ) 14 | 15 | // TargetManagerFactory is a type representing a function which builds 16 | // a TargetManager. 17 | type TargetManagerFactory func() TargetManager 18 | 19 | // TargetManagerLoader is a type representing a function which returns all the 20 | // needed things to be able to load a TestStep. 21 | type TargetManagerLoader func() (string, TargetManagerFactory) 22 | 23 | // TargetManager is an interface used to acquire and release the targets to 24 | // run tests on. 25 | type TargetManager interface { 26 | ValidateAcquireParameters([]byte) (interface{}, error) 27 | ValidateReleaseParameters([]byte) (interface{}, error) 28 | Acquire(ctx context.Context, jobID types.JobID, jobTargetManagerAcquireTimeout time.Duration, parameters interface{}, tl Locker) ([]*Target, error) 29 | Release(ctx context.Context, jobID types.JobID, targets []*Target, parameters interface{}) error 30 | } 31 | 32 | // TargetManagerBundle bundles the selected TargetManager together with its 33 | // acquire and release parameters based on the content of the job descriptor 34 | type TargetManagerBundle struct { 35 | TargetManager TargetManager 36 | AcquireParameters interface{} 37 | ReleaseParameters interface{} 38 | } 39 | -------------------------------------------------------------------------------- /pkg/test/fetcher.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package test 7 | 8 | import "context" 9 | 10 | // TestFetcherFactory is a type representing a function which builds 11 | // a TestFetcher 12 | type TestFetcherFactory func() TestFetcher 13 | 14 | // TestFetcherLoader is a type representing a function which returns all the 15 | // needed things to be able to load a TestFetcher 16 | type TestFetcherLoader func() (string, TestFetcherFactory) 17 | 18 | // TestFetcher is an interface used to get the test to run on the selected 19 | // hosts. 20 | type TestFetcher interface { 21 | ValidateFetchParameters(context.Context, []byte, bool) (interface{}, error) 22 | Fetch(context.Context, interface{}) (string, []*TestStepDescriptor, error) 23 | } 24 | 25 | // TestFetcherBundle bundles the selected TestFetcher together with its acquire 26 | // and release parameters based on the content of the job descriptor. 27 | // The bundle contains also the selected TestFetcher for the cleanup steps, together 28 | // with its parameters. 29 | type TestFetcherBundle struct { 30 | TestFetcher TestFetcher 31 | FetchParameters interface{} 32 | CleanupFetcher TestFetcher 33 | CleanupParameters interface{} 34 | } 35 | -------------------------------------------------------------------------------- /pkg/test/parameter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package test 7 | 8 | import ( 9 | "bytes" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "text/template" 14 | 15 | "github.com/linuxboot/contest/pkg/target" 16 | ) 17 | 18 | // NewParam inititalizes a new Param object directly from a string. 19 | // Note that no validation is performed if the input is actually valid JSON. 20 | func NewParam(s string) *Param { 21 | var p Param 22 | p.RawMessage = json.RawMessage(s) 23 | return &p 24 | } 25 | 26 | // Param represents a test step parameter. It is initialized from JSON, 27 | // and can be a string or a more complex JSON structure. 28 | // Plugins are expected to know which one they expect and use the 29 | // provided convenience functions to obtain either the string or 30 | // json.RawMessage representation. 31 | type Param struct { 32 | json.RawMessage 33 | } 34 | 35 | // IsEmpty returns true if the original raw string is empty, false otherwise. 36 | func (p Param) IsEmpty() bool { 37 | return p.String() == "" 38 | } 39 | 40 | // String returns the parameter as a string. This helper never fails. 41 | // If the underlying JSON cannot be unmarshalled into a simple string, 42 | // this function returns a string representation of the JSON structure. 43 | func (p Param) String() string { 44 | var str string 45 | if json.Unmarshal(p.RawMessage, &str) == nil { 46 | return str 47 | } 48 | // can't unmarshal to string, return raw json 49 | return string(p.RawMessage) 50 | } 51 | 52 | // JSON returns the parameter as json.RawMessage for further 53 | // unmarshalling by the test plugin. 54 | func (p Param) JSON() json.RawMessage { 55 | return p.RawMessage 56 | } 57 | 58 | // Expand evaluates the raw expression and applies the necessary manipulation, 59 | // if any. 60 | func (p *Param) Expand(target *target.Target, vars StepsVariablesReader) (string, error) { 61 | if p == nil { 62 | return "", errors.New("parameter cannot be nil") 63 | } 64 | funcs := getFuncMap() 65 | if target != nil && vars != nil { 66 | registerStepVariableAccessor(funcs, target.ID, vars) 67 | } 68 | // use Go text/template from here 69 | tmpl, err := template.New("").Funcs(funcs).Parse(p.String()) 70 | if err != nil { 71 | return "", fmt.Errorf("failed to parse template: %v", err) 72 | } 73 | var buf bytes.Buffer 74 | if err := tmpl.Execute(&buf, target); err != nil { 75 | return "", err 76 | } 77 | return buf.String(), nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/test/result.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package test 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/linuxboot/contest/pkg/lib/comparison" 12 | "github.com/linuxboot/contest/pkg/target" 13 | ) 14 | 15 | // GetResult evaluates the success of list of Targets based on a comparison expression 16 | func GetResult(targets map[*target.Target]error, ignore []*target.Target, expression string) (*comparison.Result, error) { 17 | var success, fail uint64 18 | for t, v := range targets { 19 | // Evaluate whether the Target is in the ignore list 20 | var skip bool 21 | for _, ignoreTarget := range ignore { 22 | skip = false 23 | if t.ID == ignoreTarget.ID { 24 | skip = true 25 | break 26 | } 27 | } 28 | if skip { 29 | continue 30 | } 31 | if v == nil { 32 | success++ 33 | } else { 34 | fail++ 35 | } 36 | } 37 | cmpExpr, err := comparison.ParseExpression(expression) 38 | if err != nil { 39 | return nil, fmt.Errorf("error while calculating test results: %v", err) 40 | } 41 | res, err := cmpExpr.EvaluateSuccess(success, success+fail) 42 | if err != nil { 43 | return nil, fmt.Errorf("error while calculating test results: %v", err) 44 | } 45 | return res, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/test/step_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package test 7 | 8 | import ( 9 | "encoding/json" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type paramSubStructure struct { 16 | Val1 string 17 | Val2 string 18 | More_nesting map[string]string 19 | } 20 | 21 | func TestTestStepParametersUnmarshalNested(t *testing.T) { 22 | descriptor := `{ 23 | "str": ["some string"], 24 | "num": [12], 25 | "substruct": [{ 26 | "Val1": "foo", 27 | "Val2": "bar", 28 | "More_nesting": { 29 | "foobar": "baz" 30 | } 31 | }] 32 | }` 33 | 34 | var params TestStepParameters 35 | err := json.Unmarshal([]byte(descriptor), ¶ms) 36 | require.NoError(t, err) 37 | require.Equal(t, "some string", params.GetOne("str").String()) 38 | num, err := params.GetInt("num") 39 | require.NoError(t, err) 40 | require.Equal(t, int64(12), num) 41 | 42 | // must be able to unmarshal substruct futher 43 | var substruct paramSubStructure 44 | err = json.Unmarshal(params.GetOne("substruct").JSON(), &substruct) 45 | require.NoError(t, err) 46 | require.Equal(t, "foo", substruct.Val1) 47 | require.Equal(t, "bar", substruct.Val2) 48 | require.Equal(t, "baz", substruct.More_nesting["foobar"]) 49 | } 50 | 51 | func TestCheckVariableName(t *testing.T) { 52 | require.NoError(t, CheckIdentifier("Abc123_XYZ")) 53 | require.Error(t, CheckIdentifier("")) 54 | require.Error(t, CheckIdentifier("1AAA")) 55 | require.Error(t, CheckIdentifier("a b")) 56 | require.Error(t, CheckIdentifier("a()+b")) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/test/test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package test 7 | 8 | import ( 9 | "encoding/json" 10 | "errors" 11 | 12 | "github.com/insomniacslk/xjson" 13 | "github.com/linuxboot/contest/pkg/target" 14 | ) 15 | 16 | // RetryParameters describes optional parameters for retry 17 | type RetryParameters struct { 18 | NumRetries uint32 19 | RetryInterval xjson.Duration 20 | } 21 | 22 | // Test describes a test definition. 23 | type Test struct { 24 | Name string 25 | TestStepsBundles []TestStepBundle 26 | TargetManagerBundle *target.TargetManagerBundle 27 | TestFetcherBundle *TestFetcherBundle 28 | RetryParameters RetryParameters 29 | CleanupStepsBundles []TestStepBundle 30 | } 31 | 32 | // TestDescriptor models the JSON encoded blob which is given as input to the 33 | // job creation request. The test descriptors are part of the main JobDescriptor 34 | // JSON document. 35 | type TestDescriptor struct { 36 | // Disabled allows to disable the test 37 | Disabled bool 38 | 39 | RetryParameters RetryParameters 40 | 41 | // TargetManager-related parameters 42 | TargetManagerName string 43 | TargetManagerAcquireParameters json.RawMessage 44 | TargetManagerReleaseParameters json.RawMessage 45 | 46 | // TestFetcher-related parameters 47 | TestFetcherName string 48 | TestFetcherFetchParameters json.RawMessage 49 | 50 | // Cleanup steps parameters 51 | CleanupFetcherName string 52 | CleanupFetcherFetchParameters json.RawMessage 53 | } 54 | 55 | // Validate performs sanity checks on the Descriptor 56 | func (d *TestDescriptor) Validate() error { 57 | if d.TargetManagerName == "" { 58 | return errors.New("target manager name cannot be empty") 59 | } 60 | if d.TestFetcherName == "" { 61 | return errors.New("test fetcher name cannot be empty") 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /pkg/transport/transport.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package transport 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/linuxboot/contest/pkg/api" 12 | "github.com/linuxboot/contest/pkg/job" 13 | "github.com/linuxboot/contest/pkg/types" 14 | ) 15 | 16 | // Transport abstracts different ways of talking to contest server. 17 | // This interface strictly only uses contest data structures. 18 | type Transport interface { 19 | Version(ctx context.Context, requestor string) (*api.VersionResponse, error) 20 | Start(ctx context.Context, requestor string, jobDescriptor string) (*api.StartResponse, error) 21 | Stop(ctx context.Context, requestor string, jobID types.JobID) (*api.StopResponse, error) 22 | Status(ctx context.Context, requestor string, jobID types.JobID) (*api.StatusResponse, error) 23 | Retry(ctx context.Context, requestor string, jobID types.JobID) (*api.RetryResponse, error) 24 | List(ctx context.Context, requestor string, states []job.State, tags []string) (*api.ListResponse, error) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/types/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package types 7 | 8 | import ( 9 | "context" 10 | "strconv" 11 | ) 12 | 13 | // JobID represents a unique job identifier 14 | type JobID uint64 15 | 16 | // RunID represents the id of a run within the Job 17 | type RunID uint64 18 | 19 | // JobOwnerID represents a unique owner identifier of a given job 20 | type JobOwnerID uint64 21 | 22 | func (v JobID) String() string { 23 | return strconv.FormatUint(uint64(v), 10) 24 | } 25 | 26 | func (v RunID) String() string { 27 | return strconv.FormatUint(uint64(v), 10) 28 | } 29 | 30 | func (v JobOwnerID) String() string { 31 | return strconv.FormatUint(uint64(v), 10) 32 | } 33 | 34 | type key string 35 | 36 | const ( 37 | KeyJobID = key("job_id") 38 | KeyRunID = key("run_id") 39 | KeyJobOwnerID = key("job_owner_id") 40 | ) 41 | 42 | // JobIDFromContext is a helper to get the JobID, this is useful 43 | // for plugins which need to know which job they are running. 44 | // Not all context object everywhere have this set, but this is 45 | // guaranteed to work in TargetManagers, TestSteps and Reporters 46 | func JobIDFromContext(ctx context.Context) (JobID, bool) { 47 | v, ok := ctx.Value(KeyJobID).(JobID) 48 | return v, ok 49 | } 50 | 51 | // RunIDFromContext is a helper to get the RunID. 52 | // Not all context object everywhere have this set, but this is 53 | // guaranteed to work in TargetManagers, TestSteps and RunReporters 54 | func RunIDFromContext(ctx context.Context) (RunID, bool) { 55 | v, ok := ctx.Value(KeyRunID).(RunID) 56 | return v, ok 57 | } 58 | 59 | // JobOwnerIDFromContext is a helper to get the JobOwnerID. 60 | // Not all context object everywhere have this set, but this is 61 | // guaranteed to work in TargetManagers, TestSteps and RunReporters 62 | func JobOwnerIDFromContext(ctx context.Context) (JobOwnerID, bool) { 63 | v, ok := ctx.Value(KeyJobOwnerID).(JobOwnerID) 64 | return v, ok 65 | } 66 | -------------------------------------------------------------------------------- /pkg/userfunctions/donothing/donothing.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package donothing 7 | 8 | import "errors" 9 | 10 | var userFunctions = map[string]interface{}{ 11 | // sample function to prove that function registration works. 12 | "do_nothing": func(a ...string) (string, error) { 13 | if len(a) == 0 { 14 | return "", errors.New("do_nothing: no arg specified") 15 | } 16 | return a[0], nil 17 | }, 18 | } 19 | 20 | // Load - Return the user-defined functions 21 | func Load() map[string]interface{} { 22 | return userFunctions 23 | } 24 | -------------------------------------------------------------------------------- /plugins/reporters/noop/noop.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package noop 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/linuxboot/contest/pkg/event/testevent" 12 | "github.com/linuxboot/contest/pkg/job" 13 | ) 14 | 15 | // Name defines the name of the reporter used within the plugin registry 16 | var Name = "noop" 17 | 18 | // Noop is a reporter that does nothing. Probably only useful for testing. 19 | type Noop struct{} 20 | 21 | // ValidateRunParameters validates the parameters for the run reporter 22 | func (n *Noop) ValidateRunParameters(params []byte) (interface{}, error) { 23 | var s string 24 | return s, nil 25 | } 26 | 27 | // ValidateFinalParameters validates the parameters for the final reporter 28 | func (n *Noop) ValidateFinalParameters(params []byte) (interface{}, error) { 29 | var s string 30 | return s, nil 31 | } 32 | 33 | // Name returns the Name of the reporter 34 | func (n *Noop) Name() string { 35 | return Name 36 | } 37 | 38 | // RunReport calculates the report to be associated with a job run. 39 | func (n *Noop) RunReport(ctx context.Context, parameters interface{}, runStatus *job.RunStatus, ev testevent.Fetcher) (bool, interface{}, error) { 40 | return true, "I did nothing", nil 41 | } 42 | 43 | // FinalReport calculates the final report to be associated to a job. 44 | func (n *Noop) FinalReport(ctx context.Context, parameters interface{}, runStatuses []job.RunStatus, ev testevent.Fetcher) (bool, interface{}, error) { 45 | return true, "I did nothing at the end, all good", nil 46 | } 47 | 48 | // New builds a new TargetSuccessReporter 49 | func New() job.Reporter { 50 | return &Noop{} 51 | } 52 | 53 | // Load returns the name and factory which are needed to register the Reporter 54 | func Load() (string, job.ReporterFactory) { 55 | return Name, New 56 | } 57 | -------------------------------------------------------------------------------- /plugins/storage/memory/memory_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package memory 7 | 8 | import ( 9 | "context" 10 | "sort" 11 | "testing" 12 | "time" 13 | 14 | "github.com/facebookincubator/go-belt/tool/logger" 15 | "github.com/linuxboot/contest/pkg/event/testevent" 16 | "github.com/linuxboot/contest/pkg/logging" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | var ( 21 | ctx = logging.WithBelt(context.Background(), logger.LevelDebug) 22 | ) 23 | 24 | func TestMemory_GetTestEvents(t *testing.T) { 25 | stor, err := New() 26 | require.NoError(t, err) 27 | 28 | ev0 := testevent.Event{ 29 | EmitTime: time.Now(), 30 | Header: &testevent.Header{ 31 | JobID: 1, 32 | RunID: 2, 33 | TestName: "3", 34 | TestStepLabel: "4", 35 | }, 36 | Data: &testevent.Data{}, 37 | } 38 | err = stor.StoreTestEvent(ctx, ev0) 39 | require.NoError(t, err) 40 | 41 | ev1 := testevent.Event{ 42 | EmitTime: time.Now(), 43 | Header: &testevent.Header{ 44 | JobID: 1, 45 | RunID: 5, 46 | TestName: "test_name_1", 47 | TestStepLabel: "test_label_1", 48 | }, 49 | Data: &testevent.Data{}, 50 | } 51 | err = stor.StoreTestEvent(ctx, ev1) 52 | require.NoError(t, err) 53 | 54 | ev2 := testevent.Event{ 55 | EmitTime: time.Now(), 56 | Header: &testevent.Header{ 57 | JobID: 1, 58 | RunID: 5, 59 | TestName: "test_name_2", 60 | TestStepLabel: "test_label_2", 61 | }, 62 | Data: &testevent.Data{}, 63 | } 64 | err = stor.StoreTestEvent(ctx, ev2) 65 | require.NoError(t, err) 66 | 67 | query, err := testevent.BuildQuery( 68 | testevent.QueryRunID(5), 69 | testevent.QueryTestNames([]string{"test_name_1", "test_name_2"}), 70 | ) 71 | require.NoError(t, err) 72 | 73 | evs, err := stor.GetTestEvents(ctx, query) 74 | require.NoError(t, err) 75 | 76 | require.Len(t, evs, 2) 77 | sort.Slice(evs, func(i, j int) bool { 78 | return evs[i].SequenceID < evs[j].SequenceID 79 | }) 80 | 81 | requireEqualExpectSequenceID := func(t *testing.T, lev testevent.Event, rev testevent.Event) { 82 | lev.SequenceID = 0 83 | rev.SequenceID = 0 84 | require.Equal(t, lev, rev) 85 | } 86 | requireEqualExpectSequenceID(t, ev1, evs[0]) 87 | requireEqualExpectSequenceID(t, ev2, evs[1]) 88 | } 89 | -------------------------------------------------------------------------------- /plugins/targetlocker/noop/noop.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // Package noop implements a no-op target locker. 7 | package noop 8 | 9 | import ( 10 | "context" 11 | "time" 12 | 13 | "github.com/linuxboot/contest/pkg/logging" 14 | "github.com/linuxboot/contest/pkg/target" 15 | "github.com/linuxboot/contest/pkg/types" 16 | ) 17 | 18 | // Name is the name used to look this plugin up. 19 | var Name = "Noop" 20 | 21 | // Noop is the no-op target locker. It does nothing. 22 | type Noop struct { 23 | } 24 | 25 | // Lock locks the specified targets by doing nothing. 26 | func (tl Noop) Lock(ctx context.Context, _ types.JobID, _ time.Duration, targets []*target.Target) error { 27 | logging.Infof(ctx, "Locked %d targets by doing nothing", len(targets)) 28 | return nil 29 | } 30 | 31 | // TryLock locks the specified targets by doing nothing. 32 | func (tl Noop) TryLock(ctx context.Context, _ types.JobID, _ time.Duration, targets []*target.Target, limit uint) ([]string, error) { 33 | logging.Infof(ctx, "Trylocked %d targets by doing nothing", len(targets)) 34 | res := make([]string, 0, len(targets)) 35 | for _, t := range targets { 36 | res = append(res, t.ID) 37 | } 38 | return res, nil 39 | } 40 | 41 | // Unlock unlocks the specified targets by doing nothing. 42 | func (tl Noop) Unlock(ctx context.Context, _ types.JobID, targets []*target.Target) error { 43 | logging.Infof(ctx, "Unlocked %d targets by doing nothing", len(targets)) 44 | return nil 45 | } 46 | 47 | // RefreshLocks refreshes all the locks by the internal (non-existing) timeout, 48 | // by flawlessly doing nothing. 49 | func (tl Noop) RefreshLocks(ctx context.Context, jobID types.JobID, _ time.Duration, targets []*target.Target) error { 50 | logging.Infof(ctx, "All %d target locks are refreshed, since I had to do nothing", len(targets)) 51 | return nil 52 | } 53 | 54 | func (tl Noop) Close() error { 55 | return nil 56 | } 57 | 58 | // New initializes and returns a new ExampleTestStep. 59 | func New() target.Locker { 60 | return &Noop{} 61 | } 62 | -------------------------------------------------------------------------------- /plugins/testfetchers/literal/literal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // Package literal implements a test fetcher that embeds the test step 7 | // definitions, instead of fetching them. 8 | package literal 9 | 10 | import ( 11 | "context" 12 | "encoding/json" 13 | "fmt" 14 | 15 | "github.com/linuxboot/contest/pkg/test" 16 | ) 17 | 18 | // Name defined the name of the plugin 19 | var ( 20 | Name = "Literal" 21 | ) 22 | 23 | // FetchParameters contains the parameters necessary to fetch tests. This 24 | // structure is populated from a JSON blob. 25 | type FetchParameters struct { 26 | TestName string 27 | Steps []*test.TestStepDescriptor 28 | } 29 | 30 | // Literal implements contest.TestFetcher interface, returning dummy test fetcher 31 | type Literal struct { 32 | } 33 | 34 | // ValidateFetchParameters performs sanity checks on the fields of the 35 | // parameters that will be passed to Fetch. 36 | func (tf Literal) ValidateFetchParameters(_ context.Context, params []byte, requireName bool) (interface{}, error) { 37 | var fp FetchParameters 38 | if err := json.Unmarshal(params, &fp); err != nil { 39 | return nil, err 40 | } 41 | if requireName && fp.TestName == "" { 42 | return nil, fmt.Errorf("test name cannot be empty for fetch parameters") 43 | } 44 | return fp, nil 45 | } 46 | 47 | // Fetch returns the information necessary to build a Test object. The returned 48 | // values are: 49 | // * Name of the test 50 | // * list of step definitions 51 | // * an error if any 52 | func (tf *Literal) Fetch(_ context.Context, params interface{}) (string, []*test.TestStepDescriptor, error) { 53 | fetchParams, ok := params.(FetchParameters) 54 | if !ok { 55 | return "", nil, fmt.Errorf("Fetch expects uri.FetchParameters object") 56 | } 57 | return fetchParams.TestName, fetchParams.Steps, nil 58 | } 59 | 60 | // New initializes the TestFetcher object 61 | func New() test.TestFetcher { 62 | return &Literal{} 63 | } 64 | 65 | // Load returns the name and factory which are needed to register the 66 | // TestFetcher. 67 | func Load() (string, test.TestFetcherFactory) { 68 | return Name, New 69 | } 70 | -------------------------------------------------------------------------------- /plugins/teststeps/echo/echo.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package echo 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "errors" 12 | 13 | "github.com/linuxboot/contest/pkg/event" 14 | "github.com/linuxboot/contest/pkg/event/testevent" 15 | "github.com/linuxboot/contest/pkg/logging" 16 | "github.com/linuxboot/contest/pkg/test" 17 | "github.com/linuxboot/contest/pkg/types" 18 | ) 19 | 20 | // Name is the name used to look this plugin up. 21 | var Name = "Echo" 22 | 23 | // Events defines the events that a TestStep is allow to emit 24 | var Events = []event.Name{} 25 | 26 | // Step implements an echo-style printing plugin. 27 | type Step struct{} 28 | 29 | // New initializes and returns a new EchoStep. It implements the TestStepFactory 30 | // interface. 31 | func New() test.TestStep { 32 | return &Step{} 33 | } 34 | 35 | // Load returns the name, factory and events which are needed to register the step. 36 | func Load() (string, test.TestStepFactory, []event.Name) { 37 | return Name, New, Events 38 | } 39 | 40 | // ValidateParameters validates the parameters that will be passed to the Run 41 | // and Resume methods of the test step. 42 | func (e Step) ValidateParameters(_ context.Context, params test.TestStepParameters) error { 43 | if t := params.GetOne("text"); t.IsEmpty() { 44 | return errors.New("Missing 'text' field in echo parameters") 45 | } 46 | return nil 47 | } 48 | 49 | // Name returns the name of the Step 50 | func (e Step) Name() string { 51 | return Name 52 | } 53 | 54 | // Run executes the step 55 | func (e Step) Run( 56 | ctx context.Context, 57 | ch test.TestStepChannels, 58 | ev testevent.Emitter, 59 | stepsVars test.StepsVariables, 60 | params test.TestStepParameters, 61 | resumeState json.RawMessage, 62 | ) (json.RawMessage, error) { 63 | for { 64 | select { 65 | case target, ok := <-ch.In: 66 | if !ok { 67 | return nil, nil 68 | } 69 | output, err := params.GetOne("text").Expand(target, stepsVars) 70 | if err != nil { 71 | return nil, err 72 | } 73 | // guaranteed to work here 74 | jobID, _ := types.JobIDFromContext(ctx) 75 | runID, _ := types.RunIDFromContext(ctx) 76 | logging.Infof(ctx, "This is job %d, run %d on target %s with text '%s'", jobID, runID, target.ID, output) 77 | ch.Out <- test.TestStepResult{Target: target} 78 | case <-ctx.Done(): 79 | return nil, nil 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /plugins/teststeps/exec/exec.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package exec 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | 13 | "github.com/insomniacslk/xjson" 14 | "github.com/linuxboot/contest/pkg/event" 15 | "github.com/linuxboot/contest/pkg/event/testevent" 16 | "github.com/linuxboot/contest/pkg/test" 17 | 18 | "github.com/linuxboot/contest/plugins/teststeps" 19 | ) 20 | 21 | type stepParams struct { 22 | Bin struct { 23 | Path string `json:"path"` 24 | Args []string `json:"args"` 25 | // TODO: add max execution timer 26 | } `json:"bin"` 27 | 28 | Transport struct { 29 | Proto string `json:"proto"` 30 | Options json.RawMessage `json:"options,omitempty"` 31 | } `json:"transport,omitempty"` 32 | 33 | OCPOutput bool `json:"ocp_output"` 34 | 35 | Constraints struct { 36 | TimeQuota xjson.Duration `json:"time_quota,omitempty"` 37 | } `json:"constraints,omitempty"` 38 | 39 | ExitCodeMap map[int]string `json:"exitcode_map,omitempty"` 40 | } 41 | 42 | // Name is the name used to look this plugin up. 43 | var Name = "Exec" 44 | 45 | // TestStep implementation for the exec plugin 46 | type TestStep struct { 47 | stepParams 48 | } 49 | 50 | // Name returns the name of the Step 51 | func (ts TestStep) Name() string { 52 | return Name 53 | } 54 | 55 | // Run executes the step. 56 | func (ts *TestStep) Run( 57 | ctx context.Context, 58 | ch test.TestStepChannels, 59 | ev testevent.Emitter, 60 | stepsVars test.StepsVariables, 61 | params test.TestStepParameters, 62 | resumeState json.RawMessage, 63 | ) (json.RawMessage, error) { 64 | if err := ts.populateParams(params); err != nil { 65 | return nil, err 66 | } 67 | 68 | tr := NewTargetRunner(ts, ev, stepsVars) 69 | return teststeps.ForEachTarget(Name, ctx, ch, tr.Run) 70 | } 71 | 72 | func (ts *TestStep) populateParams(stepParams test.TestStepParameters) error { 73 | bag := stepParams.GetOne("bag").JSON() 74 | 75 | if err := json.Unmarshal(bag, &ts.stepParams); err != nil { 76 | return fmt.Errorf("failed to deserialize parameters") 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // ValidateParameters validates the parameters associated to the step 83 | func (ts *TestStep) ValidateParameters(_ context.Context, stepParams test.TestStepParameters) error { 84 | return ts.populateParams(stepParams) 85 | } 86 | 87 | // New initializes and returns a new exec step. 88 | func New() test.TestStep { 89 | return &TestStep{} 90 | } 91 | 92 | // Load returns the name, factory and events which are needed to register the step. 93 | func Load() (string, test.TestStepFactory, []event.Name) { 94 | return Name, New, Events 95 | } 96 | -------------------------------------------------------------------------------- /plugins/teststeps/exec/ocp_parser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package exec 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/linuxboot/contest/pkg/event/testevent" 15 | 16 | "github.com/stretchr/testify/mock" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | type mockEmitter struct { 21 | mock.Mock 22 | 23 | Calls []testevent.Data 24 | } 25 | 26 | func (e *mockEmitter) Emit(ctx context.Context, data testevent.Data) error { 27 | e.Calls = append(e.Calls, data) 28 | 29 | args := e.Called(ctx, data) 30 | return args.Error(0) 31 | } 32 | 33 | func TestOCPEventParser(t *testing.T) { 34 | ctx := context.Background() 35 | 36 | ev := &mockEmitter{} 37 | ev.On("Emit", ctx, mock.Anything).Return(nil) 38 | 39 | data := `{"testRunArtifact":{"testRunEnd":{"name":"Error Monitor","status":"COMPLETE","result":"PASS"}},"sequenceNumber":1,"timestamp":"ts"}` 40 | r := strings.NewReader(data) 41 | 42 | p := NewOCPEventParser(nil, ev) 43 | dec := json.NewDecoder(r) 44 | for dec.More() { 45 | var root *OCPRoot 46 | err := dec.Decode(&root) 47 | require.NoError(t, err) 48 | 49 | err = p.Parse(ctx, root) 50 | require.NoError(t, err) 51 | } 52 | 53 | require.Equal(t, 1, len(ev.Calls)) 54 | require.Equal(t, ev.Calls[0].EventName, TestEndEvent) 55 | 56 | var payload testEndEventPayload 57 | err := json.Unmarshal(*ev.Calls[0].Payload, &payload) 58 | require.NoError(t, err) 59 | 60 | require.Equal(t, payload.Result, "PASS") 61 | } 62 | -------------------------------------------------------------------------------- /plugins/teststeps/exec/transport/local_transport.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | //go:build unsafe 7 | // +build unsafe 8 | 9 | package transport 10 | 11 | import ( 12 | "context" 13 | "errors" 14 | "fmt" 15 | "io" 16 | "os/exec" 17 | 18 | "github.com/linuxboot/contest/pkg/logging" 19 | ) 20 | 21 | type LocalTransport struct{} 22 | 23 | func NewLocalTransport() Transport { 24 | return &LocalTransport{} 25 | } 26 | 27 | func (lt *LocalTransport) NewProcess(ctx context.Context, bin string, args []string) (Process, error) { 28 | return newLocalProcess(ctx, bin, args) 29 | } 30 | 31 | // localProcess is just a thin layer over exec.Command 32 | type localProcess struct { 33 | cmd *exec.Cmd 34 | } 35 | 36 | func newLocalProcess(ctx context.Context, bin string, args []string) (Process, error) { 37 | if err := checkBinary(bin); err != nil { 38 | return nil, err 39 | } 40 | 41 | cmd := exec.CommandContext(ctx, bin, args...) 42 | return &localProcess{cmd}, nil 43 | } 44 | 45 | func (lp *localProcess) Start(ctx context.Context) error { 46 | logging.Debugf(ctx, "starting local binary: %v", lp) 47 | if err := lp.cmd.Start(); err != nil { 48 | return fmt.Errorf("failed to start process: %w", err) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (lp *localProcess) Wait(_ context.Context) error { 55 | if err := lp.cmd.Wait(); err != nil { 56 | var e *exec.ExitError 57 | if errors.As(err, &e) { 58 | return &ExitError{e.ExitCode()} 59 | } 60 | 61 | return fmt.Errorf("failed to wait on process: %w", err) 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func (lp *localProcess) StdoutPipe() (io.Reader, error) { 68 | stdout, err := lp.cmd.StdoutPipe() 69 | if err != nil { 70 | return nil, fmt.Errorf("failed to get stdout pipe") 71 | } 72 | return stdout, nil 73 | } 74 | 75 | func (lp *localProcess) StderrPipe() (io.Reader, error) { 76 | stderr, err := lp.cmd.StderrPipe() 77 | if err != nil { 78 | return nil, fmt.Errorf("failed to get stdout pipe") 79 | } 80 | return stderr, nil 81 | } 82 | 83 | func (lp *localProcess) String() string { 84 | return lp.cmd.String() 85 | } 86 | -------------------------------------------------------------------------------- /plugins/teststeps/exec/transport/local_transport_safe.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | //go:build !unsafe 7 | // +build !unsafe 8 | 9 | package transport 10 | 11 | import ( 12 | "context" 13 | "fmt" 14 | ) 15 | 16 | type LocalTransport struct{} 17 | 18 | func NewLocalTransport() Transport { 19 | return &LocalTransport{} 20 | } 21 | 22 | func (lt *LocalTransport) NewProcess(ctx context.Context, bin string, args []string) (Process, error) { 23 | return nil, fmt.Errorf("unavailable without unsafe build tag") 24 | } 25 | -------------------------------------------------------------------------------- /plugins/teststeps/exec/transport/transport.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package transport 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | "io" 13 | 14 | "github.com/linuxboot/contest/pkg/test" 15 | ) 16 | 17 | type Transport interface { 18 | NewProcess(ctx context.Context, bin string, args []string) (Process, error) 19 | } 20 | 21 | // ExitError is returned by Process.Wait when the controlled process exited with 22 | // a non-zero exit code (depending on transport) 23 | type ExitError struct { 24 | ExitCode int 25 | } 26 | 27 | func (e *ExitError) Error() string { 28 | return fmt.Sprintf("process exited with non-zero code: %d", e.ExitCode) 29 | } 30 | 31 | type Process interface { 32 | Start(ctx context.Context) error 33 | Wait(ctx context.Context) error 34 | 35 | StdoutPipe() (io.Reader, error) 36 | StderrPipe() (io.Reader, error) 37 | 38 | String() string 39 | } 40 | 41 | func NewTransport(proto string, configSource json.RawMessage, expander *test.ParamExpander) (Transport, error) { 42 | switch proto { 43 | case "local": 44 | return NewLocalTransport(), nil 45 | 46 | case "ssh": 47 | configTempl := DefaultSSHTransportConfig() 48 | if err := json.Unmarshal(configSource, &configTempl); err != nil { 49 | return nil, fmt.Errorf("unable to deserialize transport options: %w", err) 50 | } 51 | 52 | var config SSHTransportConfig 53 | if err := expander.ExpandObject(configTempl, &config); err != nil { 54 | return nil, err 55 | } 56 | 57 | return NewSSHTransport(config), nil 58 | 59 | default: 60 | return nil, fmt.Errorf("no such transport: %v", proto) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /plugins/teststeps/exec/transport/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package transport 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "sync" 12 | "syscall" 13 | ) 14 | 15 | type deferedStack struct { 16 | funcs []func() 17 | 18 | closed bool 19 | done chan struct{} 20 | 21 | mu sync.Mutex 22 | } 23 | 24 | func newDeferedStack() *deferedStack { 25 | s := &deferedStack{nil, false, make(chan struct{}), sync.Mutex{}} 26 | 27 | go func() { 28 | <-s.done 29 | for i := len(s.funcs) - 1; i >= 0; i-- { 30 | s.funcs[i]() 31 | } 32 | }() 33 | 34 | return s 35 | } 36 | 37 | func (s *deferedStack) Add(f func()) { 38 | s.mu.Lock() 39 | defer s.mu.Unlock() 40 | 41 | s.funcs = append(s.funcs, f) 42 | } 43 | 44 | func (s *deferedStack) Done() { 45 | s.mu.Lock() 46 | defer s.mu.Unlock() 47 | 48 | if s.closed { 49 | return 50 | } 51 | 52 | close(s.done) 53 | s.closed = true 54 | } 55 | 56 | func canExecute(fi os.FileInfo) bool { 57 | // TODO: deal with acls? 58 | stat := fi.Sys().(*syscall.Stat_t) 59 | if stat.Uid == uint32(os.Getuid()) { 60 | return stat.Mode&0500 == 0500 61 | } 62 | 63 | if stat.Gid == uint32(os.Getgid()) { 64 | return stat.Mode&0050 == 0050 65 | } 66 | 67 | return stat.Mode&0005 == 0005 68 | } 69 | 70 | func checkBinary(bin string) error { 71 | // check binary exists and is executable 72 | fi, err := os.Stat(bin) 73 | if err != nil { 74 | return fmt.Errorf("no such file") 75 | } 76 | 77 | if !fi.Mode().IsRegular() { 78 | return fmt.Errorf("not a file") 79 | } 80 | 81 | if !canExecute(fi) { 82 | return fmt.Errorf("provided binary is not executable") 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /plugins/teststeps/qemu/README.md: -------------------------------------------------------------------------------- 1 | # Qemu Teststep 2 | 3 | 4 | ## Parameters 5 | 6 | ### Required Parameters 7 | * **executable:** Name of the qemu executable. It can be an absolute path or the name of a executable in $PATH. 8 | 9 | * **firmware:** The firmware image you want to test. 10 | 11 | 12 | ### Optional Paramters 13 | * **logfile:** The output of the running image is copied here. If left empty the output will be discarded by setting the logfile to /dev/null. 14 | 15 | * **mem:** The amount of RAM dedicated to the qemu instance in MB. 16 | 17 | * **nproc:** The amount of threads available to qemu. 18 | 19 | * **image:** A Disk Image, which can be booted by the firmware. 20 | 21 | * **timeout:** The time intervall until the qemu instance is forcibly shut down. Example: '4m' 22 | 23 | * **steps:** This is a list of steps, which can consist of expect or send steps. An expect steps expects a certain output from the virtual machine. A send step will send a string to qemu. Expect steps can have an additional timeout field, which is a string, like '2m'. If left empty the **timeout** Parameter is used as timeout instead. Make sure the timout you set for an expect step is shorter than the overall timeout. A step can have both an expect as well as a send statement; this is interpreted as an expect step, which is followed by a send step if it is successful. The steps are executed in order beginning from the first entry. Each step is blocking, meaning the next step will be executed only if the previous step was successful. 24 | Example: 25 | steps: 26 | - expect: 'Welcome .*please login:' 27 | timeout: 3s 28 | send: username 29 | - expect: Password 30 | - send: secretPassword 31 | - expect: Login successful 32 | Notice that regular expressions have to be surrounded by single quotes. 33 | 34 | - name: qemu 35 | label: awesome test 36 | parameters: 37 | executable: ['qemu-system-aarch64] 38 | firmware: ['/my/awesome/firmware'] 39 | image: ['/home/user1/images/Linux.qcow2'] 40 | nproc: [8] 41 | mem: [8000] 42 | logfile: [/tmp/Logfile] 43 | timeout: [4m] 44 | steps: 45 | - expect: Booting into OS 46 | timeout: 4s 47 | - expect: '\nKernel' 48 | - expect: login 49 | - send: user 50 | - expect: Password 51 | - timeout: 5s 52 | - send: 12345password 53 | - expect: user@ 54 | - timeout: 10s 55 | - send: poweroff 56 | - expect: Power down 57 | -------------------------------------------------------------------------------- /plugins/teststeps/variables/readme.md: -------------------------------------------------------------------------------- 1 | # Variables plugin 2 | 3 | The *variables* plugin adds its input parameters as step plugin variables that could be later referred to by other plugins. 4 | 5 | ## Parameters 6 | 7 | Any parameter should be a single-value parameter that will be added as a test-variable. 8 | For example: 9 | 10 | { 11 | "name": "variables", 12 | "label": "variablesstep" 13 | "parameters": { 14 | "string_variable": ["Hello"], 15 | "int_variable": [123], 16 | "complex_variable": [{"hello": "world"}] 17 | } 18 | } 19 | 20 | Will generate a string, int and a json-object variables respectively. 21 | 22 | These parameters could be later accessed in the following manner in accordance with step variables guidance: 23 | 24 | { 25 | "name": "cmd", 26 | "label": "cmdstep", 27 | "parameters": { 28 | "executable": [echo], 29 | "args": ["{{ StringVar \"variablesstep.string_variable\" }} world number {{ IntVar \"variablesstep.int_variable\" }}"], 30 | "emit_stdout": [true], 31 | "emit_stderr": [true] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /plugins/teststeps/variables/variables.go: -------------------------------------------------------------------------------- 1 | package variables 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/linuxboot/contest/pkg/event" 9 | "github.com/linuxboot/contest/pkg/event/testevent" 10 | "github.com/linuxboot/contest/pkg/logging" 11 | "github.com/linuxboot/contest/pkg/target" 12 | "github.com/linuxboot/contest/pkg/test" 13 | 14 | "github.com/linuxboot/contest/plugins/teststeps" 15 | ) 16 | 17 | // Name is the name used to look this plugin up. 18 | const Name = "variables" 19 | 20 | // Events defines the events that a TestStep is allowed to emit 21 | var Events []event.Name 22 | 23 | // Variables creates variables that can be used by other test steps 24 | type Variables struct { 25 | } 26 | 27 | // Name returns the plugin name. 28 | func (ts *Variables) Name() string { 29 | return Name 30 | } 31 | 32 | // Run executes the cmd step. 33 | func (ts *Variables) Run( 34 | ctx context.Context, 35 | ch test.TestStepChannels, 36 | ev testevent.Emitter, 37 | stepsVars test.StepsVariables, 38 | inputParams test.TestStepParameters, 39 | resumeState json.RawMessage, 40 | ) (json.RawMessage, error) { 41 | if err := ts.ValidateParameters(ctx, inputParams); err != nil { 42 | return nil, err 43 | } 44 | return teststeps.ForEachTarget(Name, ctx, ch, func(ctx context.Context, target *target.Target) error { 45 | for name, ps := range inputParams { 46 | logging.Debugf(ctx, "add variable %s, value: %s", name, ps[0]) 47 | if err := stepsVars.Add(target.ID, name, ps[0].RawMessage); err != nil { 48 | return err 49 | } 50 | } 51 | return nil 52 | }) 53 | } 54 | 55 | // ValidateParameters validates the parameters associated to the TestStep 56 | func (ts *Variables) ValidateParameters(ctx context.Context, params test.TestStepParameters) error { 57 | for name, ps := range params { 58 | if err := test.CheckIdentifier(name); err != nil { 59 | return fmt.Errorf("invalid variable name: '%s': %w", name, err) 60 | } 61 | if len(ps) != 1 { 62 | return fmt.Errorf("invalid number of parameter '%s' values: %d (expected 1)", name, len(ps)) 63 | } 64 | 65 | var res interface{} 66 | if err := json.Unmarshal(ps[0].RawMessage, &res); err != nil { 67 | return fmt.Errorf("invalid json '%s': %w", ps[0].RawMessage, err) 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | // New initializes and returns a new Variables test step. 74 | func New() test.TestStep { 75 | return &Variables{} 76 | } 77 | 78 | // Load returns the name, factory and events which are needed to register the step. 79 | func Load() (string, test.TestStepFactory, []event.Name) { 80 | return Name, New, Events 81 | } 82 | -------------------------------------------------------------------------------- /plugins/teststeps/waitport/waitport_test.go: -------------------------------------------------------------------------------- 1 | package waitport 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/linuxboot/contest/pkg/event/testevent" 11 | "github.com/linuxboot/contest/pkg/storage" 12 | "github.com/linuxboot/contest/pkg/target" 13 | "github.com/linuxboot/contest/pkg/test" 14 | 15 | "github.com/linuxboot/contest/plugins/storage/memory" 16 | ) 17 | 18 | func TestWaitForTCPPort(t *testing.T) { 19 | listener, err := net.Listen("tcp", ":0") 20 | if err != nil { 21 | t.Fatalf("Failed to start listening TCP port: '%v'", err) 22 | } 23 | defer func() { 24 | if err := listener.Close(); err != nil { 25 | t.Errorf("Failed to close listener: '%v'", err) 26 | } 27 | }() 28 | 29 | ctx, cancel := context.WithCancel(context.Background()) 30 | defer cancel() 31 | 32 | m, err := memory.New() 33 | if err != nil { 34 | t.Fatalf("could not initialize memory storage: '%v'", err) 35 | } 36 | storageEngineVault := storage.NewSimpleEngineVault() 37 | if err := storageEngineVault.StoreEngine(m, storage.SyncEngine); err != nil { 38 | t.Fatalf("Failed to set memory storage: '%v'", err) 39 | } 40 | 41 | var wg sync.WaitGroup 42 | wg.Add(1) 43 | go func() { 44 | wg.Done() 45 | for ctx.Err() == nil { 46 | conn, err := listener.Accept() 47 | if err == nil || conn == nil { 48 | continue 49 | } 50 | _ = conn.Close() 51 | } 52 | }() 53 | 54 | inCh := make(chan *target.Target, 1) 55 | testStepChannels := test.TestStepChannels{ 56 | In: inCh, 57 | Out: make(chan test.TestStepResult, 1), 58 | } 59 | ev := storage.NewTestEventEmitterFetcher(storageEngineVault, testevent.Header{ 60 | JobID: 12345, 61 | TestName: "waitport_tests", 62 | TestStepLabel: "waitport", 63 | }) 64 | 65 | inCh <- &target.Target{ 66 | ID: "some_id", 67 | FQDN: "localhost", 68 | } 69 | close(inCh) 70 | 71 | params := test.TestStepParameters{ 72 | "protocol": []test.Param{*test.NewParam("tcp")}, 73 | "port": []test.Param{*test.NewParam(fmt.Sprintf("%d", listener.Addr().(*net.TCPAddr).Port))}, 74 | "timeout": []test.Param{*test.NewParam("2m")}, 75 | "check_interval": []test.Param{*test.NewParam("10ms")}, 76 | } 77 | 78 | plugin := &WaitPort{} 79 | if _, err = plugin.Run(ctx, testStepChannels, ev, nil, params, nil); err != nil { 80 | t.Errorf("Plugin run failed: '%v'", err) 81 | } 82 | wg.Wait() 83 | } 84 | -------------------------------------------------------------------------------- /run_lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright (c) Facebook, Inc. and its affiliates. 4 | # 5 | # This source code is licensed under the MIT license found in the 6 | # LICENSE file in the root directory of this source tree. 7 | 8 | set -exu 9 | 10 | export GO111MODULE=on 11 | # installing golangci-lint as recommended on the project page 12 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest 13 | go mod download 14 | golangci-lint run --disable typecheck --enable staticcheck --timeout 10m 15 | 16 | # check license headers 17 | # this needs to be run from the top level directory, because it uses 18 | # `git ls-files` under the hood. 19 | go get -u github.com/u-root/u-root/tools/checklicenses 20 | go install github.com/u-root/u-root/tools/checklicenses 21 | echo "[*] Running checklicenses" 22 | go run github.com/u-root/u-root/tools/checklicenses -c tools/checklicenses-config.json 23 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright (c) Facebook, Inc. and its affiliates. 4 | # 5 | # This source code is licensed under the MIT license found in the 6 | # LICENSE file in the root directory of this source tree. 7 | 8 | set -eu 9 | 10 | export DATABASE=${database:-mysql} 11 | export CI=${CI:-false} 12 | export DOCKER_BUILDKIT=1 13 | export COMPOSE_DOCKER_CLI_BUILD=1 14 | 15 | while getopts d: flag 16 | do 17 | case "${flag}" in 18 | d) DATABASE=${OPTARG};; 19 | esac 20 | done 21 | 22 | if [ "${UID}" -ne 0 ] 23 | then 24 | echo "Re-running as root with sudo" 25 | sudo --preserve-env=DATABASE,CI,GITHUB_ACTIONS,GITHUB_REF,GITHUB_REPOSITORY,GITHUB_HEAD_REF,GITHUB_SHA,GITHUB_RUN_ID,PATH "$0" "$@" 26 | exit $? 27 | fi 28 | 29 | COMPOSE_FILE=docker-compose.yml 30 | if [ "${DATABASE}" != "mysql" ] 31 | then 32 | COMPOSE_FILE=docker-compose.$DATABASE.yml 33 | fi 34 | 35 | codecov_env=`bash <(curl -s https://codecov.io/env)` 36 | docker compose -f $COMPOSE_FILE build $DATABASE contest 37 | docker compose -f $COMPOSE_FILE run \ 38 | ${codecov_env} -e "CI=${CI}" \ 39 | contest \ 40 | /go/src/github.com/linuxboot/contest/docker/contest/tests.sh \ 41 | -------------------------------------------------------------------------------- /tests/common/get_events.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package common 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "strings" 12 | 13 | "github.com/linuxboot/contest/pkg/event" 14 | "github.com/linuxboot/contest/pkg/event/testevent" 15 | "github.com/linuxboot/contest/pkg/storage" 16 | "github.com/linuxboot/contest/pkg/types" 17 | ) 18 | 19 | func eventToStringNoTime(ev testevent.Event) string { 20 | // Omit the timestamp to make output stable. 21 | return fmt.Sprintf("{%s%s}", ev.Header, ev.Data) 22 | } 23 | 24 | func getEventsAsString(ctx context.Context, st storage.EventStorage, jobID types.JobID, testNames []string, eventNames []event.Name, targetID, stepLabel *string) string { 25 | var qp []testevent.QueryField 26 | if jobID != 0 { 27 | qp = append(qp, testevent.QueryJobID(jobID)) 28 | } 29 | if len(testNames) > 0 { 30 | qp = append(qp, testevent.QueryTestNames(testNames)) 31 | } 32 | if len(eventNames) > 0 { 33 | qp = append(qp, testevent.QueryEventNames(eventNames)) 34 | } 35 | q, _ := testevent.BuildQuery(qp...) 36 | results, _ := st.GetTestEvents(ctx, q) 37 | var resultsForTarget []string 38 | for _, r := range results { 39 | if targetID != nil { 40 | if r.Data.Target == nil { 41 | continue 42 | } 43 | if *targetID != "" && r.Data.Target.ID != *targetID { 44 | continue 45 | } 46 | } 47 | if stepLabel != nil { 48 | if *stepLabel != "" && r.Header.TestStepLabel != *stepLabel { 49 | continue 50 | } 51 | if targetID == nil && r.Data.Target != nil { 52 | continue 53 | } 54 | } 55 | resultsForTarget = append(resultsForTarget, eventToStringNoTime(r)) 56 | } 57 | return "\n" + strings.Join(resultsForTarget, "\n") + "\n" 58 | } 59 | 60 | // GetJobEventsAsString queries storage for particular test's events, 61 | // further filtering by target ID and/or step label. 62 | func GetJobEventsAsString(ctx context.Context, st storage.EventStorage, jobID types.JobID, eventNames []event.Name) string { 63 | return getEventsAsString(ctx, st, jobID, nil, eventNames, nil, nil) 64 | } 65 | 66 | // GetTestEventsAsString queries storage for particular test's events, 67 | // further filtering by target ID and/or step label. 68 | func GetTestEventsAsString(ctx context.Context, st storage.EventStorage, testNames []string, targetID, stepLabel *string) string { 69 | return getEventsAsString(ctx, st, 0, testNames, nil, targetID, stepLabel) 70 | } 71 | -------------------------------------------------------------------------------- /tests/common/goroutine_leak_check/goroutine_leak_check_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package goroutine_leak_check 7 | 8 | import ( 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func Func1(c *sync.Cond) { 17 | c.L.Lock() 18 | c.Signal() 19 | c.Wait() 20 | c.L.Unlock() 21 | } 22 | 23 | func TestNoLeaks(t *testing.T) { 24 | require.NoError(t, CheckLeakedGoRoutines()) 25 | } 26 | 27 | func TestLeaks(t *testing.T) { 28 | c := sync.NewCond(&sync.Mutex{}) 29 | c.L.Lock() 30 | go Func1(c) 31 | c.Wait() // Wait for the go routine to start. 32 | c.L.Unlock() 33 | err := CheckLeakedGoRoutines() 34 | require.Contains(t, err.Error(), "goroutine_leak_check_test.go") 35 | require.Contains(t, err.Error(), "goroutine_leak_check.Func1") 36 | c.Signal() 37 | // Give some time for the goroutine to exit 38 | // otherwise it causes flakes in case of multiple test runs (-count=N). 39 | time.Sleep(10 * time.Millisecond) 40 | } 41 | 42 | func TestLeaksWhitelisted(t *testing.T) { 43 | c := sync.NewCond(&sync.Mutex{}) 44 | c.L.Lock() 45 | go Func1(c) 46 | c.Wait() 47 | c.L.Unlock() 48 | require.NoError(t, CheckLeakedGoRoutines( 49 | "github.com/linuxboot/contest/tests/common/goroutine_leak_check.Func1")) 50 | c.Signal() 51 | time.Sleep(10 * time.Millisecond) 52 | } 53 | -------------------------------------------------------------------------------- /tests/e2e/test-resume.yaml: -------------------------------------------------------------------------------- 1 | JobName: A job to test resumption 2 | Runs: 2 3 | RunInterval: 3s 4 | TestDescriptors: 5 | - TargetManagerName: TargetListWithState 6 | TargetManagerAcquireParameters: 7 | Targets: 8 | - ID: T1 9 | TargetManagerReleaseParameters: 10 | TestFetcherName: literal 11 | TestFetcherFetchParameters: 12 | TestName: Test 1 13 | Steps: 14 | - name: cmd 15 | label: Test1Step1 16 | parameters: 17 | executable: [echo] 18 | args: ["Test 1, Step 1, target {{ .ID }}"] 19 | emit_stdout: [true] 20 | emit_stderr: [true] 21 | - name: sleep # Supports pause / resume. 22 | label: Test1Step2 23 | parameters: 24 | duration: [2s] 25 | - name: sleep # Supports pause / resume. 26 | label: Test1Step3 27 | parameters: 28 | duration: [2s] 29 | - name: cmd 30 | label: Test1Step4 31 | parameters: 32 | executable: [echo] 33 | args: ["Test 1, Step 3, target {{ .ID }}"] 34 | emit_stdout: [true] 35 | emit_stderr: [true] 36 | - TargetManagerName: TargetListWithState 37 | TargetManagerAcquireParameters: 38 | Targets: 39 | - ID: T2 40 | TargetManagerReleaseParameters: 41 | TestFetcherName: literal 42 | TestFetcherFetchParameters: 43 | TestName: Test 2 44 | Steps: 45 | - name: cmd 46 | label: Test2Step1 47 | parameters: 48 | executable: [echo] 49 | args: ["Test 2, Step 1, target {{ .ID }}"] 50 | emit_stdout: [true] 51 | emit_stderr: [true] 52 | - name: cmd # Does not support pause, will have to wait. 53 | label: Test2Step2 54 | parameters: 55 | executable: [sleep] 56 | args: [2] 57 | emit_stdout: [true] 58 | emit_stderr: [true] 59 | - name: cmd 60 | label: Test2Step3 61 | parameters: 62 | executable: [echo] 63 | args: ["Test 2, Step 3, target {{ .ID }}"] 64 | emit_stdout: [true] 65 | emit_stderr: [true] 66 | Reporting: 67 | RunReporters: 68 | - name: TargetSuccess 69 | parameters: 70 | SuccessExpression: "=100%" 71 | - name: noop 72 | FinalReporters: 73 | - name: noop 74 | -------------------------------------------------------------------------------- /tests/e2e/test-retry.yaml: -------------------------------------------------------------------------------- 1 | JobName: A job to test retries 2 | Runs: 1 3 | TestDescriptors: 4 | - RetryParameters: 5 | NumRetries: 1 6 | RetryInterval: "10s" 7 | TargetManagerName: TargetListWithState 8 | TargetManagerAcquireParameters: 9 | Targets: 10 | - ID: T1 11 | TargetManagerReleaseParameters: 12 | TestFetcherName: literal 13 | TestFetcherFetchParameters: 14 | TestName: Test 1 15 | Steps: 16 | - name: cmd 17 | label: Step1 18 | parameters: 19 | executable: [echo] 20 | args: ["Test 1, Step 1, target {{ .ID }}"] 21 | emit_stdout: [true] 22 | emit_stderr: [true] 23 | - name: waitport 24 | label: Step2 25 | parameters: 26 | target: ["localhost"] 27 | port: ["[[ .WaitPort]]"] 28 | check_interval: ["50ms"] 29 | protocol: ["tcp"] 30 | timeout: ["500ms"] 31 | - name: cmd 32 | label: Step3 33 | parameters: 34 | executable: [ echo ] 35 | args: [ "Test 1, Step 1, target {{ .ID }}" ] 36 | emit_stdout: [ true ] 37 | emit_stderr: [ true ] 38 | Reporting: 39 | RunReporters: 40 | - name: TargetSuccess 41 | parameters: 42 | SuccessExpression: "=100%" 43 | - name: noop 44 | FinalReporters: 45 | - name: noop 46 | -------------------------------------------------------------------------------- /tests/e2e/test-simple.yaml: -------------------------------------------------------------------------------- 1 | JobName: A simple test job 2 | Runs: 2 3 | RunInterval: 1s 4 | Tags: 5 | - test 6 | - simple 7 | TestDescriptors: 8 | - TargetManagerName: TargetList 9 | TargetManagerAcquireParameters: 10 | Targets: 11 | - ID: T1 12 | TargetManagerReleaseParameters: 13 | TestFetcherName: literal 14 | TestFetcherFetchParameters: 15 | TestName: Test 1 16 | Steps: 17 | - name: cmd 18 | label: Test1Step1 19 | parameters: 20 | executable: [echo] 21 | args: ["Test 1, Step 1, target {{ .ID }}"] 22 | emit_stdout: [true] 23 | emit_stderr: [true] 24 | - name: cmd 25 | label: Test1Step2 26 | parameters: 27 | executable: [echo] 28 | args: ["Test 1, Step 2, target {{ .ID }}"] 29 | emit_stdout: [true] 30 | emit_stderr: [true] 31 | - TargetManagerName: TargetList 32 | TargetManagerAcquireParameters: 33 | Targets: 34 | - ID: T2 35 | TargetManagerReleaseParameters: 36 | TestFetcherName: literal 37 | TestFetcherFetchParameters: 38 | TestName: Test 2 39 | Steps: 40 | - name: cmd 41 | label: Test2Step1 42 | parameters: 43 | executable: [echo] 44 | args: ["Test 2, Step 1, target {{ .ID }}"] 45 | emit_stdout: [true] 46 | emit_stderr: [true] 47 | - name: cmd 48 | label: Test2Step2 49 | parameters: 50 | executable: [echo] 51 | args: ["Test 2, Step 2, target {{ .ID }}"] 52 | emit_stdout: [true] 53 | emit_stderr: [true] 54 | Reporting: 55 | RunReporters: 56 | - name: TargetSuccess 57 | parameters: 58 | SuccessExpression: "=100%" 59 | FinalReporters: 60 | - name: noop 61 | -------------------------------------------------------------------------------- /tests/e2e/test-variables.yaml: -------------------------------------------------------------------------------- 1 | JobName: A variables test job 2 | Runs: 1 3 | RunInterval: 1s 4 | Tags: 5 | - test 6 | - variables 7 | TestDescriptors: 8 | - TargetManagerName: TargetList 9 | TargetManagerAcquireParameters: 10 | Targets: 11 | - ID: T1 12 | TargetManagerReleaseParameters: 13 | TestFetcherName: literal 14 | TestFetcherFetchParameters: 15 | TestName: Test 1 16 | Steps: 17 | - name: variables 18 | label: variablesstep 19 | parameters: 20 | message: ["Hello"] 21 | - name: cmd 22 | label: cmdstep 23 | parameters: 24 | executable: [echo] 25 | args: ["{{ StringVar \"variablesstep.message\" }}"] 26 | emit_stdout: [true] 27 | emit_stderr: [true] 28 | Reporting: 29 | RunReporters: 30 | - name: TargetSuccess 31 | parameters: 32 | SuccessExpression: "=100%" 33 | FinalReporters: 34 | - name: noop -------------------------------------------------------------------------------- /tests/integ/admin_server/flags_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration_admin 2 | // +build integration_admin 3 | 4 | package test 5 | 6 | import ( 7 | "flag" 8 | "time" 9 | ) 10 | 11 | var ( 12 | flagAdminEndpoint = flag.String("adminServer", "http://adminserver:8000/log", "admin server log push endpoint") 13 | flagAdminProjectEndpoint = flag.String("ProjectEndpoint", "http://adminserver:8000/tag", "admin server project query endpoint") 14 | flagMongoEndpoint = flag.String("mongoDBURI", "mongodb://mongostorage:27017", "mongodb URI") 15 | flagContestDBURI = flag.String("ContestDBURI", "contest:contest@tcp(dbstorage:3306)/contest_integ?parseTime=true", "contest db URI") 16 | flagOperationTimeout = flag.Duration("operationTimeout", time.Duration(10*time.Second), "operation timeout duration") 17 | ) 18 | -------------------------------------------------------------------------------- /tests/integ/common/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package common 7 | 8 | import ( 9 | "os" 10 | 11 | "github.com/linuxboot/contest/pkg/storage" 12 | "github.com/linuxboot/contest/plugins/storage/rdbms" 13 | ) 14 | 15 | func GetDatabaseURI() string { 16 | if os.Getenv("CI") != "" { 17 | return "contest:contest@tcp(dbstorage:3306)/contest_integ?parseTime=true" 18 | } else { 19 | return "contest:contest@tcp(localhost:3306)/contest_integ?parseTime=true" 20 | } 21 | } 22 | 23 | func NewStorage(opts ...rdbms.Opt) (storage.Storage, error) { 24 | return rdbms.New(GetDatabaseURI(), opts...) 25 | } 26 | 27 | // InitStorage initializes the storage backend with a new transaction, if supported 28 | func InitStorage(s storage.Storage) storage.Storage { 29 | switch s := s.(type) { 30 | case storage.ResettableStorage: 31 | _ = s.Reset() 32 | default: 33 | panic("unknown storage type") 34 | } 35 | return s 36 | } 37 | 38 | // FinalizeStorage finalizes the storage layer with either a rollback of the current transaction 39 | // or by resetting altogether the backend, if supported. 40 | func FinalizeStorage(s storage.Storage) { 41 | switch s := s.(type) { 42 | case storage.ResettableStorage: 43 | _ = s.Reset() 44 | default: 45 | panic("unknown storage type") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/integ/events/frameworkevents/frameworkevents_memory_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // +build integration 7 | 8 | package test 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/linuxboot/contest/plugins/storage/memory" 15 | "github.com/stretchr/testify/suite" 16 | ) 17 | 18 | func TestFrameworkEventsSuiteMemoryStorage(t *testing.T) { 19 | 20 | testSuite := FrameworkEventsSuite{} 21 | // Run the TestSuite with memory storage layer 22 | storagelayer, err := memory.New() 23 | if err != nil { 24 | panic(fmt.Sprintf("could not initialize in-memory storage layer: %v", err)) 25 | } 26 | testSuite.storage = storagelayer 27 | suite.Run(t, &testSuite) 28 | } 29 | -------------------------------------------------------------------------------- /tests/integ/events/frameworkevents/frameworkevents_rdbms_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // +build integration_storage 7 | 8 | package test 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | "time" 14 | 15 | "github.com/linuxboot/contest/plugins/storage/rdbms" 16 | "github.com/linuxboot/contest/tests/integ/common" 17 | "github.com/stretchr/testify/suite" 18 | ) 19 | 20 | func TestFrameworkEventsSuiteRdbmsStorage(t *testing.T) { 21 | 22 | testSuite := FrameworkEventsSuite{} 23 | 24 | opts := []rdbms.Opt{ 25 | rdbms.FrameworkEventsFlushSize(0), 26 | rdbms.FrameworkEventsFlushInterval(10 * time.Second), 27 | } 28 | storageLayer, err := common.NewStorage(opts...) 29 | if err != nil { 30 | panic(fmt.Sprintf("could not initialize rdbms storage layer: %v", err)) 31 | } 32 | 33 | testSuite.storage = storageLayer 34 | suite.Run(t, &testSuite) 35 | } 36 | -------------------------------------------------------------------------------- /tests/integ/events/testevents/testevents_memory_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // +build integration 7 | 8 | package test 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/linuxboot/contest/pkg/storage" 15 | "github.com/linuxboot/contest/plugins/storage/memory" 16 | "github.com/stretchr/testify/require" 17 | "github.com/stretchr/testify/suite" 18 | ) 19 | 20 | func TestTestEventsSuiteMemoryStorage(t *testing.T) { 21 | 22 | vault := storage.NewSimpleEngineVault() 23 | testSuite := NewTestEventSuite(vault) 24 | // Run the TestSuite with memory storage layer 25 | storagelayer, err := memory.New() 26 | if err != nil { 27 | panic(fmt.Sprintf("could not initialize in-memory storage layer: %v", err)) 28 | } 29 | testSuite.storage = storagelayer 30 | 31 | err = vault.StoreEngine(storagelayer, storage.SyncEngine) 32 | require.NoError(t, err) 33 | 34 | suite.Run(t, &testSuite) 35 | } 36 | -------------------------------------------------------------------------------- /tests/integ/events/testevents/testevents_rdbms_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // +build integration_storage 7 | 8 | package test 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | "time" 14 | 15 | "github.com/linuxboot/contest/plugins/storage/rdbms" 16 | "github.com/linuxboot/contest/tests/integ/common" 17 | 18 | "github.com/stretchr/testify/suite" 19 | ) 20 | 21 | func TestTestEventsSuiteRdbmsStorage(t *testing.T) { 22 | testSuite := TestEventsSuite{} 23 | 24 | opts := []rdbms.Opt{ 25 | rdbms.TestEventsFlushSize(0), 26 | rdbms.TestEventsFlushInterval(10 * time.Second), 27 | } 28 | storageLayer, err := common.NewStorage(opts...) 29 | if err != nil { 30 | panic(fmt.Sprintf("could not initialize rdbms storage layer: %v", err)) 31 | 32 | } 33 | testSuite.storage = storageLayer 34 | suite.Run(t, &testSuite) 35 | } 36 | -------------------------------------------------------------------------------- /tests/integ/job/job_memory_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // +build integration 7 | 8 | package test 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/linuxboot/contest/plugins/storage/memory" 15 | "github.com/stretchr/testify/suite" 16 | ) 17 | 18 | func TestJobSuiteMemoryStorage(t *testing.T) { 19 | 20 | testSuite := JobSuite{} 21 | // Run the TestSuite with memory storage layer 22 | storagelayer, err := memory.New() 23 | if err != nil { 24 | panic(fmt.Sprintf("could not initialize in-memory storage layer: %v", err)) 25 | } 26 | testSuite.storage = storagelayer 27 | suite.Run(t, &testSuite) 28 | } 29 | -------------------------------------------------------------------------------- /tests/integ/job/job_rdbms_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // +build integration_storage 7 | 8 | package test 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | "time" 14 | 15 | "github.com/linuxboot/contest/plugins/storage/rdbms" 16 | "github.com/linuxboot/contest/tests/integ/common" 17 | 18 | "github.com/stretchr/testify/suite" 19 | ) 20 | 21 | func TestJobSuiteRdbmsStorage(t *testing.T) { 22 | testSuite := JobSuite{} 23 | 24 | opts := []rdbms.Opt{ 25 | rdbms.TestEventsFlushSize(1), 26 | rdbms.TestEventsFlushInterval(10 * time.Second), 27 | } 28 | storageLayer, err := common.NewStorage(opts...) 29 | if err != nil { 30 | panic(fmt.Sprintf("could not initialize rdbms storage layer: %v", err)) 31 | } 32 | testSuite.storage = storageLayer 33 | suite.Run(t, &testSuite) 34 | } 35 | -------------------------------------------------------------------------------- /tests/integ/jobmanager/common_longtest.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // +build integration integration_storage 7 | // +build longtest 8 | 9 | package test 10 | 11 | import ( 12 | "syscall" 13 | "time" 14 | 15 | "github.com/linuxboot/contest/pkg/jobmanager" 16 | ) 17 | 18 | func (suite *TestJobManagerSuite) TestWaitAndExit() { 19 | suite.testExit(syscall.SIGUSR1, jobmanager.EventJobCompleted, 10*time.Second) 20 | } 21 | -------------------------------------------------------------------------------- /tests/integ/jobmanager/jobmanager_memory_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // +build integration 7 | 8 | package test 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/linuxboot/contest/plugins/storage/memory" 15 | 16 | "github.com/stretchr/testify/suite" 17 | ) 18 | 19 | func TestJobManagerSuiteMemoryStorage(t *testing.T) { 20 | 21 | testSuite := TestJobManagerSuite{} 22 | // Run the TestSuite with memory storage layer 23 | storagelayer, err := memory.New() 24 | if err != nil { 25 | panic(fmt.Sprintf("could not initialize in-memory storage layer: %v", err)) 26 | 27 | } 28 | testSuite.storage = storagelayer 29 | suite.Run(t, &testSuite) 30 | } 31 | -------------------------------------------------------------------------------- /tests/integ/jobmanager/jobmanager_rdbms_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | // +build integration_storage 7 | 8 | package test 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | "time" 14 | 15 | "github.com/linuxboot/contest/plugins/storage/rdbms" 16 | "github.com/linuxboot/contest/tests/integ/common" 17 | 18 | "github.com/stretchr/testify/suite" 19 | ) 20 | 21 | func TestJobManagerSuiteRdbmsStorage(t *testing.T) { 22 | testSuite := TestJobManagerSuite{} 23 | 24 | opts := []rdbms.Opt{ 25 | rdbms.TestEventsFlushSize(1), 26 | rdbms.TestEventsFlushInterval(10 * time.Second), 27 | } 28 | storageLayer, err := common.NewStorage(opts...) 29 | if err != nil { 30 | panic(fmt.Sprintf("could not initialize rdbms storage layer: %v", err)) 31 | } 32 | 33 | testSuite.storage = storageLayer 34 | 35 | suite.Run(t, &testSuite) 36 | } 37 | -------------------------------------------------------------------------------- /tests/integ/plugins/cmd_plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | //go:build integration 7 | // +build integration 8 | 9 | package tests 10 | 11 | import ( 12 | "context" 13 | "testing" 14 | "time" 15 | 16 | "github.com/linuxboot/contest/pkg/runner" 17 | "github.com/linuxboot/contest/pkg/test" 18 | "github.com/linuxboot/contest/pkg/types" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestCmdPlugin(t *testing.T) { 24 | 25 | jobID := types.JobID(1) 26 | runID := types.RunID(1) 27 | 28 | ts1, err := pluginRegistry.NewTestStep("cmd") 29 | require.NoError(t, err) 30 | 31 | params := make(test.TestStepParameters) 32 | params["executable"] = []test.Param{ 33 | *test.NewParam("sleep"), 34 | } 35 | params["args"] = []test.Param{ 36 | *test.NewParam("5"), 37 | } 38 | 39 | testSteps := []test.TestStepBundle{ 40 | {TestStep: ts1, Parameters: params}, 41 | } 42 | 43 | stateCtx, cancel := context.WithCancel(ctx) 44 | errCh := make(chan error, 1) 45 | 46 | go func() { 47 | tr := runner.NewTestRunner() 48 | eventsFactory := runner.NewTestStepEventsEmitterFactory(storageEngineVault, jobID, runID, "", 0) 49 | _, _, _, err := tr.Run(stateCtx, &test.Test{TestStepsBundles: testSteps}, targets, eventsFactory, nil, nil, testSteps) 50 | errCh <- err 51 | }() 52 | 53 | go func() { 54 | time.Sleep(time.Second) 55 | cancel() 56 | }() 57 | 58 | select { 59 | case <-errCh: 60 | case <-time.After(successTimeout): 61 | t.Errorf("test should return within timeout: %+v", successTimeout) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/plugins/reporters/readmeta/readmeta.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package readmeta_test 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/linuxboot/contest/pkg/event/testevent" 12 | "github.com/linuxboot/contest/pkg/job" 13 | "github.com/linuxboot/contest/pkg/types" 14 | ) 15 | 16 | // Name defines the name of the reporter used within the plugin registry 17 | var Name = "readmeta" 18 | 19 | // Noop is a reporter that does nothing. Probably only useful for testing. 20 | type Readmeta struct{} 21 | 22 | // ValidateRunParameters validates the parameters for the run reporter 23 | func (n *Readmeta) ValidateRunParameters(params []byte) (interface{}, error) { 24 | var s string 25 | return s, nil 26 | } 27 | 28 | // ValidateFinalParameters validates the parameters for the final reporter 29 | func (n *Readmeta) ValidateFinalParameters(params []byte) (interface{}, error) { 30 | var s string 31 | return s, nil 32 | } 33 | 34 | // Name returns the Name of the reporter 35 | func (n *Readmeta) Name() string { 36 | return Name 37 | } 38 | 39 | // RunReport calculates the report to be associated with a job run. 40 | func (n *Readmeta) RunReport(ctx context.Context, parameters interface{}, runStatus *job.RunStatus, ev testevent.Fetcher) (bool, interface{}, error) { 41 | // test metadata exists 42 | jobID, ok1 := types.JobIDFromContext(ctx) 43 | // note this must use panic to abort the test run, as this is a test for the job runner which ignore the actual outcome 44 | if jobID == 0 || !ok1 { 45 | panic("Unable to extract jobID from context") 46 | } 47 | runID, ok2 := types.RunIDFromContext(ctx) 48 | if runID == 0 || !ok2 { 49 | panic("Unable to extract runID from context") 50 | } 51 | return true, "I did nothing", nil 52 | } 53 | 54 | // FinalReport calculates the final report to be associated to a job. 55 | func (n *Readmeta) FinalReport(ctx context.Context, parameters interface{}, runStatuses []job.RunStatus, ev testevent.Fetcher) (bool, interface{}, error) { 56 | // this one only has jobID, there is no specific runID in the final reporter 57 | jobID, ok1 := types.JobIDFromContext(ctx) 58 | // note this must use panic to abort the test run, as this is a test for the job runner which ignore the actual outcome 59 | if jobID == 0 || !ok1 { 60 | panic("Unable to extract jobID from context") 61 | } 62 | return true, "I did nothing at all", nil 63 | } 64 | 65 | // New builds a new TargetSuccessReporter 66 | func New() job.Reporter { 67 | return &Readmeta{} 68 | } 69 | 70 | // Load returns the name and factory which are needed to register the Reporter 71 | func Load() (string, job.ReporterFactory) { 72 | return Name, New 73 | } 74 | -------------------------------------------------------------------------------- /tests/plugins/targetlocker/targetlocker_dblocker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | //go:build integration_storage 7 | // +build integration_storage 8 | 9 | package targetlocker 10 | 11 | import ( 12 | "testing" 13 | "time" 14 | 15 | "github.com/benbjohnson/clock" 16 | "github.com/stretchr/testify/require" 17 | "github.com/stretchr/testify/suite" 18 | 19 | "github.com/linuxboot/contest/plugins/targetlocker/dblocker" 20 | "github.com/linuxboot/contest/tests/integ/common" 21 | ) 22 | 23 | type DBLockerTestSuite struct { 24 | TargetLockerTestSuite 25 | } 26 | 27 | func (ts *DBLockerTestSuite) SetupTest() { 28 | ts.clock = clock.NewMock() 29 | require.NotNil(ts.T(), ts.clock) 30 | ts.clock.Add(1 * time.Hour) // avoid zero time, start at 1:00 31 | tl, err := dblocker.New( 32 | common.GetDatabaseURI(), 33 | dblocker.WithClock(ts.clock), 34 | dblocker.WithMaxBatchSize(3), 35 | ) 36 | require.NoError(ts.T(), err) 37 | require.NotNil(ts.T(), tl) 38 | tl.ResetAllLocks(ctx) 39 | ts.tl = tl 40 | } 41 | 42 | func (ts *DBLockerTestSuite) TearDownTest() { 43 | ts.tl.Close() 44 | ts.tl = nil 45 | } 46 | 47 | func TestDBLocker(t *testing.T) { 48 | suite.Run(t, &DBLockerTestSuite{}) 49 | } 50 | -------------------------------------------------------------------------------- /tests/plugins/targetlocker/targetlocker_inmemory_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package targetlocker 7 | 8 | import ( 9 | "testing" 10 | "time" 11 | 12 | "github.com/benbjohnson/clock" 13 | "github.com/stretchr/testify/require" 14 | "github.com/stretchr/testify/suite" 15 | 16 | "github.com/linuxboot/contest/plugins/targetlocker/inmemory" 17 | ) 18 | 19 | type InMemoryTargetLockerTestSuite struct { 20 | TargetLockerTestSuite 21 | } 22 | 23 | func (ts *InMemoryTargetLockerTestSuite) SetupTest() { 24 | ts.clock = clock.NewMock() 25 | require.NotNil(ts.T(), ts.clock) 26 | ts.clock.Add(1 * time.Hour) // avoid zero time, start at 1:00 27 | ts.tl = inmemory.New(ts.clock) 28 | require.NotNil(ts.T(), ts.tl) 29 | } 30 | 31 | func (ts *InMemoryTargetLockerTestSuite) TearDownTest() { 32 | ts.tl.Close() 33 | ts.tl = nil 34 | } 35 | 36 | func TestInMemoryTargetLocker(t *testing.T) { 37 | suite.Run(t, &InMemoryTargetLockerTestSuite{}) 38 | } 39 | -------------------------------------------------------------------------------- /tests/plugins/targetmanagers/readmeta/readmeta.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package readmeta 7 | 8 | import ( 9 | "context" 10 | "time" 11 | 12 | "github.com/linuxboot/contest/pkg/target" 13 | "github.com/linuxboot/contest/pkg/types" 14 | ) 15 | 16 | // Name defined the name of the plugin 17 | var ( 18 | Name = "readmeta" 19 | ) 20 | 21 | // Readmeta implements the contest.TargetManager interface. 22 | type Readmeta struct { 23 | } 24 | 25 | // ValidateAcquireParameters valides parameters that will be passed to Acquire. 26 | func (r Readmeta) ValidateAcquireParameters(params []byte) (interface{}, error) { 27 | return nil, nil 28 | } 29 | 30 | // ValidateReleaseParameters valides parameters that will be passed to Release. 31 | func (r Readmeta) ValidateReleaseParameters(params []byte) (interface{}, error) { 32 | return nil, nil 33 | } 34 | 35 | // Acquire implements contest.TargetManager.Acquire 36 | func (t *Readmeta) Acquire(ctx context.Context, jobID types.JobID, jobTargetManagerAcquireTimeout time.Duration, parameters interface{}, tl target.Locker) ([]*target.Target, error) { 37 | // test metadata exists 38 | jobID2, ok1 := types.JobIDFromContext(ctx) 39 | // note this must use panic to abort the test run, as this is a test for the job runner which ignore the actual outcome 40 | if jobID2 == 0 || !ok1 { 41 | panic("Unable to extract jobID from context") 42 | } 43 | runID, ok2 := types.RunIDFromContext(ctx) 44 | if runID == 0 || !ok2 { 45 | panic("Unable to extract runID from context") 46 | } 47 | // return one target so the test continues normally 48 | return []*target.Target{{ID: "testtarget123"}}, nil 49 | } 50 | 51 | // Release releases the acquired resources. 52 | func (t *Readmeta) Release(ctx context.Context, jobID types.JobID, targets []*target.Target, params interface{}) error { 53 | // test metadata exists 54 | jobID2, ok1 := types.JobIDFromContext(ctx) 55 | // note this must use panic to abort the test run, as this is a test for the job runner which ignore the actual outcome 56 | if jobID2 == 0 || !ok1 { 57 | panic("Unable to extract jobID from context") 58 | } 59 | runID, ok2 := types.RunIDFromContext(ctx) 60 | if runID == 0 || !ok2 { 61 | panic("Unable to extract runID from context") 62 | } 63 | return nil 64 | } 65 | 66 | // New builds a new Readmeta object. 67 | func New() target.TargetManager { 68 | return &Readmeta{} 69 | } 70 | 71 | // Load returns the name and factory which are needed to register the 72 | // TargetManager. 73 | func Load() (string, target.TargetManagerFactory) { 74 | return Name, New 75 | } 76 | -------------------------------------------------------------------------------- /tests/plugins/teststeps/channels/channels.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package channels 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | 12 | "github.com/linuxboot/contest/pkg/event" 13 | "github.com/linuxboot/contest/pkg/event/testevent" 14 | "github.com/linuxboot/contest/pkg/test" 15 | ) 16 | 17 | // Name is the name used to look this plugin up. 18 | var Name = "Channels" 19 | 20 | // Events defines the events that a TestStep is allowed to emit 21 | var Events = []event.Name{} 22 | 23 | type channels struct { 24 | } 25 | 26 | // Name returns the name of the Step 27 | func (ts *channels) Name() string { 28 | return Name 29 | } 30 | 31 | // Run executes a step that runs fine but closes its output channels on exit. 32 | func (ts *channels) Run( 33 | ctx context.Context, 34 | ch test.TestStepChannels, 35 | ev testevent.Emitter, 36 | stepsVars test.StepsVariables, 37 | inputParams test.TestStepParameters, 38 | resumeState json.RawMessage, 39 | ) (json.RawMessage, error) { 40 | for target := range ch.In { 41 | ch.Out <- test.TestStepResult{Target: target} 42 | } 43 | // This is bad, do not do this. 44 | close(ch.Out) 45 | return nil, nil 46 | } 47 | 48 | // ValidateParameters validates the parameters associated to the TestStep 49 | func (ts *channels) ValidateParameters(_ context.Context, params test.TestStepParameters) error { 50 | return nil 51 | } 52 | 53 | // New creates a new Channels step 54 | func New() test.TestStep { 55 | return &channels{} 56 | } 57 | -------------------------------------------------------------------------------- /tests/plugins/teststeps/crash/crash.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package crash 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | 13 | "github.com/linuxboot/contest/pkg/event" 14 | "github.com/linuxboot/contest/pkg/event/testevent" 15 | "github.com/linuxboot/contest/pkg/test" 16 | ) 17 | 18 | // Name is the name used to look this plugin up. 19 | var Name = "Crash" 20 | 21 | // Events defines the events that a TestStep is allow to emit 22 | var Events = []event.Name{} 23 | 24 | type crash struct { 25 | } 26 | 27 | // Name returns the name of the Step 28 | func (ts *crash) Name() string { 29 | return Name 30 | } 31 | 32 | // Run executes a step which returns an error 33 | func (ts *crash) Run( 34 | ctx context.Context, 35 | ch test.TestStepChannels, 36 | ev testevent.Emitter, 37 | stepsVars test.StepsVariables, 38 | inputParams test.TestStepParameters, 39 | resumeState json.RawMessage, 40 | ) (json.RawMessage, error) { 41 | return nil, fmt.Errorf("TestStep crashed") 42 | } 43 | 44 | // ValidateParameters validates the parameters associated to the TestStep 45 | func (ts *crash) ValidateParameters(_ context.Context, params test.TestStepParameters) error { 46 | return nil 47 | } 48 | 49 | // New creates a new noop step 50 | func New() test.TestStep { 51 | return &crash{} 52 | } 53 | -------------------------------------------------------------------------------- /tests/plugins/teststeps/fail/fail.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package fail 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | 13 | "github.com/linuxboot/contest/pkg/event" 14 | "github.com/linuxboot/contest/pkg/event/testevent" 15 | "github.com/linuxboot/contest/pkg/target" 16 | "github.com/linuxboot/contest/pkg/test" 17 | 18 | "github.com/linuxboot/contest/plugins/teststeps" 19 | ) 20 | 21 | // Name is the name used to look this plugin up. 22 | var Name = "Fail" 23 | 24 | // Events defines the events that a TestStep is allow to emit 25 | var Events = []event.Name{} 26 | 27 | type fail struct { 28 | } 29 | 30 | // Name returns the name of the Step 31 | func (ts *fail) Name() string { 32 | return Name 33 | } 34 | 35 | // Run executes a step that fails all the targets it receives. 36 | func (ts *fail) Run( 37 | ctx context.Context, 38 | ch test.TestStepChannels, 39 | ev testevent.Emitter, 40 | stepsVars test.StepsVariables, 41 | params test.TestStepParameters, 42 | resumeState json.RawMessage, 43 | ) (json.RawMessage, error) { 44 | return teststeps.ForEachTarget(Name, ctx, ch, func(ctx context.Context, t *target.Target) error { 45 | return fmt.Errorf("Integration test failure for %v", t) 46 | }) 47 | } 48 | 49 | // ValidateParameters validates the parameters associated to the TestStep 50 | func (ts *fail) ValidateParameters(_ context.Context, params test.TestStepParameters) error { 51 | return nil 52 | } 53 | 54 | // New creates a new noop step 55 | func New() test.TestStep { 56 | return &fail{} 57 | } 58 | -------------------------------------------------------------------------------- /tests/plugins/teststeps/hanging/hanging.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package hanging 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | 12 | "github.com/linuxboot/contest/pkg/event" 13 | "github.com/linuxboot/contest/pkg/event/testevent" 14 | "github.com/linuxboot/contest/pkg/test" 15 | ) 16 | 17 | // Name is the name used to look this plugin up. 18 | var Name = "Hanging" 19 | 20 | // Events defines the events that a TestStep is allow to emit 21 | var Events = []event.Name{} 22 | 23 | type hanging struct { 24 | } 25 | 26 | // Name returns the name of the Step 27 | func (ts *hanging) Name() string { 28 | return Name 29 | } 30 | 31 | // Run executes a step that does not process any targets and never returns. 32 | func (ts *hanging) Run( 33 | ctx context.Context, 34 | ch test.TestStepChannels, 35 | ev testevent.Emitter, 36 | stepsVars test.StepsVariables, 37 | inputParams test.TestStepParameters, 38 | resumeState json.RawMessage, 39 | ) (json.RawMessage, error) { 40 | channel := make(chan struct{}) 41 | <-channel 42 | return nil, nil 43 | } 44 | 45 | // ValidateParameters validates the parameters associated to the TestStep 46 | func (ts *hanging) ValidateParameters(_ context.Context, params test.TestStepParameters) error { 47 | return nil 48 | } 49 | 50 | // New creates a new hanging step 51 | func New() test.TestStep { 52 | return &hanging{} 53 | } 54 | -------------------------------------------------------------------------------- /tests/plugins/teststeps/noop/noop.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package noop 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | 12 | "github.com/linuxboot/contest/pkg/event" 13 | "github.com/linuxboot/contest/pkg/event/testevent" 14 | "github.com/linuxboot/contest/pkg/target" 15 | "github.com/linuxboot/contest/pkg/test" 16 | 17 | "github.com/linuxboot/contest/plugins/teststeps" 18 | ) 19 | 20 | // Name is the name used to look this plugin up. 21 | var Name = "Noop" 22 | 23 | // Events defines the events that a TestStep is allow to emit 24 | var Events = []event.Name{} 25 | 26 | type noop struct { 27 | } 28 | 29 | // Name returns the name of the Step 30 | func (ts *noop) Name() string { 31 | return Name 32 | } 33 | 34 | // Run executes a step that does nothing and returns targets with success. 35 | func (ts *noop) Run( 36 | ctx context.Context, 37 | ch test.TestStepChannels, 38 | ev testevent.Emitter, 39 | stepsVars test.StepsVariables, 40 | inputParams test.TestStepParameters, 41 | resumeState json.RawMessage, 42 | ) (json.RawMessage, error) { 43 | return teststeps.ForEachTarget(Name, ctx, ch, func(ctx context.Context, t *target.Target) error { 44 | return nil 45 | }) 46 | } 47 | 48 | // ValidateParameters validates the parameters associated to the TestStep 49 | func (ts *noop) ValidateParameters(_ context.Context, params test.TestStepParameters) error { 50 | return nil 51 | } 52 | 53 | // New creates a new noop step 54 | func New() test.TestStep { 55 | return &noop{} 56 | } 57 | -------------------------------------------------------------------------------- /tests/plugins/teststeps/noreturn/noreturn.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package noreturn 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | 12 | "github.com/linuxboot/contest/pkg/event" 13 | "github.com/linuxboot/contest/pkg/event/testevent" 14 | "github.com/linuxboot/contest/pkg/test" 15 | ) 16 | 17 | // Name is the name used to look this plugin up. 18 | var Name = "NoReturn" 19 | 20 | // Events defines the events that a TestStep is allow to emit 21 | var Events = []event.Name{} 22 | 23 | type noreturnStep struct { 24 | } 25 | 26 | // Name returns the name of the Step 27 | func (ts *noreturnStep) Name() string { 28 | return Name 29 | } 30 | 31 | // Run executes a step that never returns. 32 | func (ts *noreturnStep) Run( 33 | ctx context.Context, 34 | ch test.TestStepChannels, 35 | ev testevent.Emitter, 36 | stepsVars test.StepsVariables, 37 | inputParams test.TestStepParameters, 38 | resumeState json.RawMessage, 39 | ) (json.RawMessage, error) { 40 | for target := range ch.In { 41 | ch.Out <- test.TestStepResult{Target: target} 42 | } 43 | channel := make(chan struct{}) 44 | <-channel 45 | return nil, nil 46 | } 47 | 48 | // ValidateParameters validates the parameters associated to the TestStep 49 | func (ts *noreturnStep) ValidateParameters(_ context.Context, params test.TestStepParameters) error { 50 | return nil 51 | } 52 | 53 | // New creates a new noreturnStep which forwards targets before hanging 54 | func New() test.TestStep { 55 | return &noreturnStep{} 56 | } 57 | -------------------------------------------------------------------------------- /tests/plugins/teststeps/panicstep/panicstep.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package panicstep 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | 12 | "github.com/linuxboot/contest/pkg/event" 13 | "github.com/linuxboot/contest/pkg/event/testevent" 14 | "github.com/linuxboot/contest/pkg/test" 15 | ) 16 | 17 | // Name is the name used to look this plugin up. 18 | var Name = "Panic" 19 | 20 | // Events defines the events that a TestStep is allow to emit 21 | var Events = []event.Name{} 22 | 23 | type panicStep struct { 24 | } 25 | 26 | // Name returns the name of the Step 27 | func (ts *panicStep) Name() string { 28 | return Name 29 | } 30 | 31 | // Run executes the example step. 32 | func (ts *panicStep) Run( 33 | ctx context.Context, 34 | ch test.TestStepChannels, 35 | ev testevent.Emitter, 36 | stepsVars test.StepsVariables, 37 | inputParams test.TestStepParameters, 38 | resumeState json.RawMessage, 39 | ) (json.RawMessage, error) { 40 | panic("panic step") 41 | } 42 | 43 | // ValidateParameters validates the parameters associated to the TestStep 44 | func (ts *panicStep) ValidateParameters(_ context.Context, params test.TestStepParameters) error { 45 | return nil 46 | } 47 | 48 | // New creates a new panicStep 49 | func New() test.TestStep { 50 | return &panicStep{} 51 | } 52 | -------------------------------------------------------------------------------- /tests/plugins/teststeps/readmeta/readmeta.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package readmeta 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | 13 | "github.com/linuxboot/contest/pkg/event" 14 | "github.com/linuxboot/contest/pkg/event/testevent" 15 | "github.com/linuxboot/contest/pkg/target" 16 | "github.com/linuxboot/contest/pkg/test" 17 | "github.com/linuxboot/contest/pkg/types" 18 | 19 | "github.com/linuxboot/contest/plugins/teststeps" 20 | ) 21 | 22 | // Name is the name used to look this plugin up. 23 | var Name = "readmeta" 24 | 25 | var MetadataEventName = event.Name("MetadataEvent") 26 | 27 | // Events defines the events that a TestStep is allow to emit 28 | var Events = []event.Name{ 29 | MetadataEventName, 30 | } 31 | 32 | type readmeta struct { 33 | } 34 | 35 | // Name returns the name of the Step 36 | func (ts *readmeta) Name() string { 37 | return Name 38 | } 39 | 40 | // Run executes a step that reads the job metadata that must be in the context and panics if it is missing. 41 | func (ts *readmeta) Run( 42 | ctx context.Context, 43 | ch test.TestStepChannels, 44 | ev testevent.Emitter, 45 | stepsVars test.StepsVariables, 46 | inputParams test.TestStepParameters, 47 | resumeState json.RawMessage, 48 | ) (json.RawMessage, error) { 49 | return teststeps.ForEachTarget(Name, ctx, ch, func(ctx context.Context, t *target.Target) error { 50 | jobID, ok1 := types.JobIDFromContext(ctx) 51 | if jobID == 0 || !ok1 { 52 | return fmt.Errorf("unable to extract jobID from context") 53 | } 54 | runID, ok2 := types.RunIDFromContext(ctx) 55 | if runID == 0 || !ok2 { 56 | return fmt.Errorf("unable to extract jobID from context") 57 | } 58 | payload := make(map[string]int) 59 | payload["job_id"] = int(jobID) 60 | payload["run_id"] = int(runID) 61 | payloadStr, err := json.Marshal(payload) 62 | if err != nil { 63 | return err 64 | } 65 | payloadJson := json.RawMessage(payloadStr) 66 | if err := ev.Emit(ctx, testevent.Data{EventName: MetadataEventName, Payload: &payloadJson}); err != nil { 67 | return err 68 | } 69 | return nil 70 | }) 71 | } 72 | 73 | // ValidateParameters validates the parameters associated to the TestStep 74 | func (ts *readmeta) ValidateParameters(_ context.Context, params test.TestStepParameters) error { 75 | return nil 76 | } 77 | 78 | // New creates a new readmeta step 79 | func New() test.TestStep { 80 | return &readmeta{} 81 | } 82 | -------------------------------------------------------------------------------- /tools/checklicenses-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "gopkg": "github.com/linuxboot/contest", 3 | "licenses": [ 4 | [ 5 | "^(//|--) Copyright \\(c\\) Facebook, Inc\\. and its affiliates\\.", 6 | "(//|--)", 7 | "(//|--) This source code is licensed under the MIT license found in the", 8 | "(//|--) LICENSE file in the root directory of this source tree\\." 9 | ] 10 | ], 11 | "accept": [ 12 | ".*\\.go", 13 | ".*\\.sql" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tools/deps.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | // 6 | // +build never 7 | 8 | package pkg 9 | 10 | import ( 11 | // Used by run_lint.sh 12 | _ "github.com/u-root/u-root/tools/checklicenses" 13 | ) 14 | -------------------------------------------------------------------------------- /tools/migration/rdbms/migrate/migrate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // 3 | // This source code is licensed under the MIT license found in the 4 | // LICENSE file in the root directory of this source tree. 5 | 6 | package migrate 7 | 8 | import ( 9 | "context" 10 | "database/sql" 11 | "runtime" 12 | ) 13 | 14 | // Migrate is the interface that every migration task must implement to support 15 | type Migrate interface { 16 | Up(tx *sql.Tx) error 17 | Down(tx *sql.Tx) error 18 | UpNoTx(db *sql.DB) error 19 | DownNoTx(db *sql.DB) error 20 | } 21 | 22 | // Factory defines a factory type of an object implementing Migration interface 23 | type Factory func(ctx context.Context) Migrate 24 | 25 | // Migration represents a migration task registered in the migration tool 26 | type Migration struct { 27 | Factory Factory 28 | Name string 29 | } 30 | 31 | // Migrations represents a sets of migrations 32 | var Migrations []Migration 33 | 34 | // Register registers a new factory for a migration 35 | func Register(Factory Factory) { 36 | _, filename, _, _ := runtime.Caller(1) 37 | newMigration := Migration{Factory: Factory, Name: filename} 38 | Migrations = append(Migrations, newMigration) 39 | } 40 | --------------------------------------------------------------------------------