diff --git a/.github/actions/run-application/action.yml b/.github/actions/run-application/action.yml new file mode 100644 index 00000000..4894da93 --- /dev/null +++ b/.github/actions/run-application/action.yml @@ -0,0 +1,54 @@ +name: 'run-application' +description: 'Run an application, wait until an expected output appears, then optionally terminate the application' +inputs: + file: + description: 'Binary to run (file name only)' + required: true + args: + description: 'Parameters for the started binary' + required: false + default: '' + wait-for: + description: 'JSON string or array of strings to search for in the stdout/stderr output of the started application' + required: true + working-directory: + description: 'The directory where to run the application' + required: true + terminate: + description: 'When set to true, the started process will be terminated' + required: false + default: 'true' +outputs: + pid: + description: 'PID of started process, only relevant when terminate is set to true' + value: ${{ steps.runapp.outputs.pid }} +runs: + using: 'composite' + steps: + - name: 'Start application in background and save the PID' + id: runapp + shell: bash + working-directory: '${{ inputs.working-directory }}' + run: | + ./${{ inputs.file }} ${{ inputs.args }} &> ${{ inputs.file }}.log & PID=$! + echo "::set-output name=pid::${PID}" + + - name: 'Wait for output to appear' + shell: bash + working-directory: '${{ inputs.working-directory }}' + run: | + python3 ${GITHUB_WORKSPACE}/examples/wait_for_output.py ${{ inputs.file }}.log '${{ inputs.wait-for }}' + + - name: 'Display log file' + shell: bash + working-directory: '${{ inputs.working-directory }}' + run: | + cat ${{ inputs.file }}.log + if: always() + + - name: 'Send SIGTERM to process with pid ${{ steps.runapp.outputs.pid }}' + shell: bash + working-directory: '${{ inputs.working-directory }}' + run: | + kill ${{ steps.runapp.outputs.pid }} + if: ${{ inputs.terminate == 'true' }} diff --git a/.github/workflows/runexamples.yml b/.github/workflows/runexamples.yml new file mode 100644 index 00000000..772818d8 --- /dev/null +++ b/.github/workflows/runexamples.yml @@ -0,0 +1,176 @@ +name: run examples + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + run-examples: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 + + - name: Build examples + shell: bash + working-directory: examples + run: | + go build -o 1-simplest/example-1 1-simplest/main.go + go build -o 2-clicks/example-2 2-clicks/main.go + go build -o 3-messaging/example-3-processor 3-messaging/cmd/processor/main.go + go build -o 3-messaging/example-3-service 3-messaging/cmd/service/main.go + go build -o 3-messaging/example-3-loadgen 3-messaging/cmd/loadgen/main.go + go build -o 3-messaging/example-3-block-user 3-messaging/cmd/block-user/main.go + go build -o 3-messaging/example-3-translate-word 3-messaging/cmd/translate-word/main.go + go build -o 5-multiple/example-5 5-multiple/main.go + go build -o 6-reconnecting-view/example-6 6-reconnecting-view/main.go + go build -o 7-redis/example-7 7-redis/*.go + go build -o 8-monitoring/example-8 8-monitoring/main.go + go build -o 9-defer-commit/example-9 9-defer-commit/main.go + go build -o 10-visit/example-10 10-visit/main.go + + - name: Start services + shell: bash + working-directory: examples + run: | + make start + + - name: Run example 1 + uses: ./.github/actions/run-application + with: + file: example-1 + wait-for: '"Processor example-group"' + working-directory: examples/1-simplest + + - name: Run example 2 + uses: ./.github/actions/run-application + with: + file: example-2 + wait-for: '"Processor mini-group"' + working-directory: examples/2-clicks + + # Example 3 is a more complex example consisting of multiple steps + - name: Start example 3 processors + id: run_example_3_processor + uses: ./.github/actions/run-application + with: + file: example-3-processor + args: -collector -blocker -filter -translator -detector + wait-for: '["Processor blocker", "Processor collector", "Processor message_filter", "Processor message_filter", "Processor message_filter"]' + working-directory: examples/3-messaging + terminate: false + + - name: Start example 3 service + id: run_example_3_service + uses: ./.github/actions/run-application + with: + file: example-3-service + args: -sent + wait-for: '"Listen port 8080"' + working-directory: examples/3-messaging + terminate: false + + - name: Start example 3 loadgen + id: run_example_3_loadgen + uses: ./.github/actions/run-application + with: + file: example-3-loadgen + wait-for: '"Posted message"' + working-directory: examples/3-messaging + terminate: false + + - name: Run example 3 list users + shell: bash + working-directory: examples/3-messaging + run: | + curl localhost:8080/Alice/feed + + - name: Run example 3 block user + shell: bash + working-directory: examples/3-messaging + run: | + ./example-3-block-user -user Bob + + - name: Run example 3 translate word + shell: bash + working-directory: examples/3-messaging + run: | + ./example-3-translate-word -word "together" -with "t°9e+her" + + - name: Stop example 3 loadgen + shell: bash + working-directory: examples/3-messaging + run: | + kill ${{ steps.run_example_3_loadgen.outputs.pid }} + + - name: Stop example 3 service + shell: bash + working-directory: examples/3-messaging + run: | + kill ${{ steps.run_example_3_service.outputs.pid }} + + - name: Stop example 3 processors + shell: bash + working-directory: examples/3-messaging + run: | + kill ${{ steps.run_example_3_processor.outputs.pid }} + + - name: Run example 4 + shell: bash + working-directory: examples/4-tests + run: | + go test example_test.go + + - name: Run example 5 + uses: ./.github/actions/run-application + with: + file: example-5 + wait-for: '"Processor multiInput"' + working-directory: examples/5-multiple + + - name: Run example 6 + uses: ./.github/actions/run-application + with: + file: example-6 + wait-for: '"View is in state"' + working-directory: examples/6-reconnecting-view + + - name: Run example 7 + uses: ./.github/actions/run-application + with: + file: example-7 + wait-for: '"Processor examples"' + working-directory: examples/7-redis + + - name: Run example 8 + uses: ./.github/actions/run-application + with: + file: example-8 + wait-for: '["Processor mini-group-stateless", "Processor mini-group-join", "Processor mini-group"]' + working-directory: examples/8-monitoring + + - name: Run example 9 + uses: ./.github/actions/run-application + with: + file: example-9 + wait-for: '["Processor forwarder", "Processor consumer"]' + working-directory: examples/9-defer-commit + + - name: Run example 10 + uses: ./.github/actions/run-application + with: + file: example-10 + wait-for: '"Processor example-visit-group"' + working-directory: examples/10-visit + + - name: Stop services + shell: bash + working-directory: examples + run: | + make stop diff --git a/examples/3-messaging/cmd/loadgen/main.go b/examples/3-messaging/cmd/loadgen/main.go index 0ddf4a6a..77e5d5a3 100644 --- a/examples/3-messaging/cmd/loadgen/main.go +++ b/examples/3-messaging/cmd/loadgen/main.go @@ -60,6 +60,7 @@ func send(from, to, content string) { log.Printf("error sending request: %v", err) return } + log.Printf("Posted message '%s' (%s -> %s)", content, from, to) defer resp.Body.Close() //TODO(diogo) check response status code } diff --git a/examples/wait_for_output.py b/examples/wait_for_output.py new file mode 100644 index 00000000..908adbc4 --- /dev/null +++ b/examples/wait_for_output.py @@ -0,0 +1,55 @@ +""" Reads a file line by line repeatedly and searches the first occurrence of one or +more strings. When all strings have been found, the script terminates with return code 0, +if TIMEOUT_AFTER_SECONDS seconds have passed it will terminate with return code 1. + +Only used in GitHub action 'run-application'. +""" + +import sys +import json +import time +import argparse + +TIMEOUT_AFTER_SECONDS = 10 + + +def wait_until_output(file_to_scan, strings_to_match): + """Reads a file line by line repeatedly and searches the first occurrence of one or + more strings, returns True if all have been found in less than TIMEOUT_AFTER_SECONDS, + else False. + """ + strings_to_match = set(strings_to_match) + + for _ in range(TIMEOUT_AFTER_SECONDS): + with open(file_to_scan) as f: + for line in f: + for curr_str in list(strings_to_match): + if curr_str in line: + strings_to_match.remove(curr_str) + if not strings_to_match: + return True + time.sleep(1) + + # not all strings found, return error + return False + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('file', help='path to file to read') + parser.add_argument('searchstrings', help='a JSON string or array of strings') + args = parser.parse_args() + + file_to_scan = args.file + strings_to_match = json.loads(args.searchstrings) + if not isinstance(strings_to_match, list): + strings_to_match = [strings_to_match] + + ret = wait_until_output(file_to_scan, strings_to_match) + + if not ret: + sys.exit(1) + + +if __name__ == '__main__': + main()