Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run examples as GitHub actions, fixes issue #379 #385

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/actions/run-application/action.yml
Original file line number Diff line number Diff line change
@@ -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' }}
176 changes: 176 additions & 0 deletions .github/workflows/runexamples.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions examples/3-messaging/cmd/loadgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
55 changes: 55 additions & 0 deletions examples/wait_for_output.py
Original file line number Diff line number Diff line change
@@ -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()