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

Add test harness #2

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
73 changes: 73 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
tests:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest, macos-latest ]
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
include:
- os: ubuntu-latest
path: ~/.cache/pip
- os: macos-latest
path: ~/Library/Caches/pip
env:
OS: ${{ matrix.os }}
PYTHON: ${{ matrix.python-version }}

defaults:
run:
shell: bash

name: Python ${{ matrix.python-version }} on OS ${{ matrix.os }}
steps:

- name: Acquire sources
uses: actions/checkout@v2

- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
architecture: x64

- uses: actions/cache@v2
with:
path: ${{ matrix.path }}
key: ${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('requirements-test.txt') }}

- name: Run tests
run: |
pip install --requirement=requirements-test.txt
pytest

- name: Upload test results to wacklig
if: always()
continue-on-error: true
env:
WACKLIG_TOKEN: ${{ secrets.WACKLIG_TOKEN }}
shell: bash
run: |
curl -s https://raw.githubusercontent.com/pipifein/wacklig-uploader/master/wacklig.py | python - --token $WACKLIG_TOKEN \
&& echo "Upload to wacklig succeeded" \
|| errcode=$?; echo "Upload to wacklig failed"; exit $errcode

# - name: Generate coverage report
# if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9'
# run: |
# coverage xml
#
# - name: Upload coverage to Codecov
# if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9'
# uses: codecov/codecov-action@v2
# with:
# files: ./coverage.xml
# flags: unittests
# env_vars: OS,PYTHON
# name: codecov-umbrella
# fail_ci_if_error: false
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/.idea
/.venv*
/test-results
/.coverage
__pycache__
32 changes: 32 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# wacklig-uploader changelog


## in progress

- Tests: Add test harness
- Fix finding test report files recursively
- Tests: Add test for `main` function
- Tests: Run test harness on GHA


## 0.3.0 (2021-07-01)

- Make glob path to pick up test results less restrictive
- Detect GitHub Action environment


## 0.2.0 (2021-02-19)

- Don't print args/ci_info but server result instead


## 0.1.0 (2021-01-28)

- Adapt upload URI
- Add token and local/git ci info
- Add license and minimal readme


## 0.0.0 (2021-01-07)

- Initial commit
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
# wacklig-uploader

A python script to upload test results to [wacklig][1].
## About

A Python script to upload test results to [wacklig].


[1]: https://wacklig.pipifein.dev/
## Tests

```shell
python3 -m venv .venv
source .venv/bin/activate
pip install --requirement=requirements-test.txt

# Run all tests.
pytest

# Run specific tests.
pytest -k test_upload_files
```

[wacklig]: https://wacklig.pipifein.dev/
24 changes: 24 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[tool.isort]
profile = "black"
extend_skip = "wacklig.py"


[tool.black]
line_length = 120
extend_exclude = "wacklig.py"


[tool.pytest.ini_options]
addopts = """
-vvv
--cov --cov-report=term-missing
--junit-xml=test-results/test/junit.xml
"""

junit_suite_name = "wacklig_uploader"

# TODO: Write captured log messages to JUnit report: one of no|log|system-out|system-err|out-err|all
# junit_logging =

# TODO: Emit XML for schema: one of legacy|xunit1|xunit2
# junit_family =
4 changes: 4 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pytest>=6,<7
pytest-mock>=3,<4
pytest-cov>=3,<4
pyfakefs>=4,<5
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import os
import sys

# Make sure that the application source directory (this directory's parent) is
# on sys.path.
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, here)
31 changes: 31 additions & 0 deletions tests/junit-example.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
<testsuite name="wacklig_uploader" errors="0" failures="1" skipped="0" tests="9" time="0.118" timestamp="2021-12-18T03:51:53.894578" hostname="sink.local">
<testcase classname="tests.test_uploader" name="test_search_env_found" time="0.001"/>
<testcase classname="tests.test_uploader" name="test_search_env_notfound" time="0.000"/>
<testcase classname="tests.test_uploader" name="test_jenkins_env" time="0.002"/>
<testcase classname="tests.test_uploader" name="test_github_action_env" time="0.002"/>
<testcase classname="tests.test_uploader" name="test_get_ci_info_jenkins" time="0.002"/>
<testcase classname="tests.test_uploader" name="test_get_ci_info_github" time="0.001"/>
<testcase classname="tests.test_uploader" name="test_get_ci_info_local" time="0.001"/>
<testcase classname="tests.test_uploader" name="test_upload_files_empty" time="0.000"/>
<testcase classname="tests.test_uploader" name="test_upload_files_foo" time="0.003">
<failure message="SystemExit: No test files found">tmp_path = PosixPath('/private/var/folders/06/w9pzygdj7vx53n_0l9q_lhph0000gn/T/pytest-of-amo/pytest-5/test_upload_files_foo0')

def test_upload_files_foo(tmp_path):
&gt; upload_files(token=None, server=None, ci_info=None, files=None)

tests/test_uploader.py:95:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

token = None, server = None, ci_info = None, files = None

def upload_files(token, server, ci_info, files):
if not files:
&gt; raise SystemExit('No test files found')
E SystemExit: No test files found

wacklig.py:76: SystemExit</failure>
</testcase>
</testsuite>
</testsuites>
78 changes: 78 additions & 0 deletions tests/test_environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os
from unittest import mock
from unittest.mock import Mock

from wacklig import get_ci_info, github_action_env, jenkins_env, search_env


@mock.patch.dict(os.environ, {"CI_FOOBAR": "bazqux"})
def test_search_env_found():
value = search_env("CI_FOOBAR")
assert value == "bazqux"


def test_search_env_notfound():
value = search_env("CI_FOOBAR")
assert value is None


@mock.patch.dict(os.environ, {"JENKINS_URL": "https://jenkins.example.org/job/3f786850e3"})
@mock.patch.dict(os.environ, {"ghprbSourceBranch": "testdrive"})
@mock.patch.dict(os.environ, {"GIT_COMMIT": "3f786850e387550fdab836ed7e6dc881de23001b"})
@mock.patch.dict(os.environ, {"ghprbPullId": "111"})
@mock.patch.dict(os.environ, {"BUILD_NUMBER": "42"})
def test_get_ci_info_jenkins():
data = get_ci_info()
assert data == {
"service": "jenkins",
"branch": "testdrive",
"commit": "3f786850e387550fdab836ed7e6dc881de23001b",
"pr": "111",
"build": "42",
}


@mock.patch.dict(os.environ, {"GITHUB_ACTION": "true"})
@mock.patch.dict(os.environ, {"GITHUB_SHA": "3f786850e387550fdab836ed7e6dc881de23001b"})
@mock.patch.dict(os.environ, {"GITHUB_REF": "refs/pull/111/merge"})
@mock.patch.dict(os.environ, {"GITHUB_HEAD_REF": "feature-branch-1"})
@mock.patch.dict(os.environ, {"GITHUB_RUN_ID": "42"})
def test_get_ci_info_github_with_pr():
data = get_ci_info()
assert data == {
"service": "github-actions",
"commit": "3f786850e387550fdab836ed7e6dc881de23001b",
"build": "42",
"branch": "feature-branch-1",
"pr": "111",
}


@mock.patch.dict(os.environ, {"GITHUB_ACTION": "true"})
@mock.patch.dict(os.environ, {"GITHUB_SHA": "3f786850e387550fdab836ed7e6dc881de23001b"})
@mock.patch.dict(os.environ, {"GITHUB_REF": "refs/heads/feature-branch-1"})
@mock.patch.dict(os.environ, {"GITHUB_HEAD_REF": ""})
@mock.patch.dict(os.environ, {"GITHUB_RUN_ID": "42"})
def test_get_ci_info_github_with_branch():
data = get_ci_info()
assert data == {
"service": "github-actions",
"commit": "3f786850e387550fdab836ed7e6dc881de23001b",
"build": "42",
"branch": "feature-branch-1",
}


@mock.patch.dict(os.environ, {"JENKINS_URL": ""})
@mock.patch.dict(os.environ, {"GITHUB_ACTION": ""})
@mock.patch(
"wacklig.check_output",
Mock(side_effect=["testdrive", "3f786850e387550fdab836ed7e6dc881de23001b"]),
)
def test_get_ci_info_local():
data = get_ci_info()
assert data == {
"service": "local",
"branch": "testdrive",
"commit": "3f786850e387550fdab836ed7e6dc881de23001b",
}
101 changes: 101 additions & 0 deletions tests/test_uploader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import io
import os
import re
import tempfile
from unittest import mock
from unittest.mock import Mock

import pytest

import wacklig
from tests.conftest import here
from wacklig import find_test_files, upload_files


def test_find_test_files(fs):
fs.create_file(file_path="test-results/test/report1.xml")
fs.create_file(file_path="test-results/test/nested/report2.xml")
fs.create_file(file_path="test-results/test/nested/more/report3.xml")
assert find_test_files() == [
"test-results/test/report1.xml",
"test-results/test/nested/report2.xml",
"test-results/test/nested/more/report3.xml",
]


def test_upload_files_empty():
with pytest.raises(SystemExit) as ex:
upload_files(token=None, server=None, ci_info=None, files=None)
assert ex.match(re.escape("No test files found"))


# Augment `NamedTemporaryFile` to not being deleted. We need it for verifying.
@mock.patch.object(wacklig.tempfile, "NamedTemporaryFile", Mock(return_value=tempfile.NamedTemporaryFile(delete=False)))
def test_upload_files_success(tmp_path, capsys):

# Prepare fixture data.
wacklig_server = "https://wacklig.example.org"
wacklig_token = "WACKLIG_TOKEN"
ci_info = {
"service": "local",
"branch": "testdrive",
"commit": "3f786850e387550fdab836ed7e6dc881de23001b",
}
test_report_files = [os.path.join(here, "tests/junit-example.xml")]

# Invoke `upload_files` with mocked `urllib.urlopen()`.
# TODO: Use a more realistic response payload here. Probably JSON?
urlopen_mock = Mock(return_value=io.BytesIO(b"HTTP response body from wacklig service"))
with mock.patch.object(wacklig, "urlopen", urlopen_mock):
upload_files(
token=wacklig_token,
server=wacklig_server,
ci_info=ci_info,
files=test_report_files,
)

# Proof that the request URL has correct information.
urlopen_mock.assert_called_once_with(
"https://wacklig.example.org/api/v1/upload?service=local&branch=testdrive&commit=3f786850e387550fdab836ed7e6dc881de23001b&token=WACKLIG_TOKEN",
data=mock.ANY,
)

# Proof that the HTTP request body content is a gzip payload.
filepath = urlopen_mock.call_args[1]["data"].name
assert open(filepath, "rb").read().startswith(b"\x1f\x8b\x08")

# TODO: Proof that it is actually a .tar.gz payload, having the correct content.

# Proof that the HTTP response body has been printed to stdout.
stdout = capsys.readouterr().out.strip()
assert stdout == "HTTP response body from wacklig service"

# Gracefully clean up temporary files.
try:
os.unlink(filepath)
except: # pragma:nocover
pass


@mock.patch.dict(os.environ, {"GITHUB_ACTION": "true"})
@mock.patch.dict(os.environ, {"GITHUB_SHA": "3f786850e387550fdab836ed7e6dc881de23001b"})
@mock.patch.dict(os.environ, {"GITHUB_RUN_ID": "42"})
def test_main(mocker, fs, capsys):

# Prepare fixture data.
wacklig_server = "https://wacklig.example.org"
wacklig_token = "WACKLIG_TOKEN"
report_files = ["test-results/test/report1.xml", "test-results/test/nested/report2.xml"]

# Create files in fake filesystem.
list(map(fs.create_file, report_files))

# Pretend to invoke main program.
mocker.patch("sys.argv", ["wacklig-upload", "--server", wacklig_server, "--token", wacklig_token])
mocker.patch.object(wacklig, "find_test_files", Mock(return_value=report_files))
mocker.patch.object(wacklig, "urlopen", Mock(return_value=io.BytesIO(b"HTTP response body from wacklig service")))
wacklig.main()

# Proof that the expected message has been written to stdout.
stdout = capsys.readouterr().out.strip()
assert "Uploaded 2 files" in stdout
Loading