commit 01a83ac43e6257d518058ce88948bba8a08feb82
parent 8d52f98134c3069733081828f707d3f95c32d2e6
Author: arjoonn <arjoonn@noreply.localhost>
Date: Sat, 25 Feb 2023 09:56:41 +0000
Add hypothesis testing (!52)
Branch auto created by JayporeCI
```jayporeci
╔ 🟢 : JayporeCI [sha 711fb2bf5c]
┏━ build-and-test
┃
┃ 🟢 : JciEnv [80e9d511] 0:30
┃ 🟢 : Jci [0e052498] 0: 9 ❮-- ['JciEnv']
┃ 🟢 : black [91d10e29] 0: 0 ❮-- ['JciEnv']
┃ 🟢 : pylint [55d481ff] 0: 8 ❮-- ['JciEnv']
┃ 🟢 : pytest [3ef1624a] 0:20 Cov: 91% ❮-- ['JciEnv']
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Publish
┃
┃ 🟢 : DockerHubJci [45ffcc42] 0:49
┃ 🟢 : DockerHubJcienv [c87a863d] 1:33
┃ 🟢 : PublishDocs [8436f6e5] 0:28
┃ 🟢 : PublishPypi [02e7e325] 0: 6 v0.2.20
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
```
Co-authored-by: arjoonn sharma <arjoonn.94@gmail.com>
Reviewed-on: https://gitea.midpathsoftware.com/midpath/jaypore_ci/pulls/52
Diffstat:
11 files changed, 134 insertions(+), 74 deletions(-)
diff --git a/cicd/run_tests.sh b/cicd/run_tests.sh
@@ -6,7 +6,7 @@ set -o pipefail
main() {
- python -m coverage run --branch --source=. -m pytest -xl --full-trace -vv
+ python -m coverage run --branch --source=. -m pytest -l --full-trace -vvvv --hypothesis-verbosity=verbose
coverage html
coverage report
echo "Cov: $(coverage report --format=total)%" > "/jaypore_ci/run/pytest.txt"
diff --git a/docs/source/index.rst b/docs/source/index.rst
@@ -392,5 +392,5 @@ Reference
.. |logo| image:: _static/logo80.png
:width: 80
- :alt: Jaypore CI logo
+ :alt: Jaypore CI
:align: middle
diff --git a/jaypore_ci/clean.py b/jaypore_ci/clean.py
@@ -0,0 +1,9 @@
+allowed_alphabet = "abcdefghijklmnopqrstuvwxyz1234567890"
+allowed_alphabet += allowed_alphabet.upper()
+
+
+def name(given):
+ """
+ Clean a given name so that it can be used inside of JCI.
+ """
+ return "".join(l if l in allowed_alphabet else "-" for l in given)
diff --git a/jaypore_ci/executors/docker.py b/jaypore_ci/executors/docker.py
@@ -5,6 +5,7 @@ import pendulum
import docker
from rich import print as rprint
+from jaypore_ci import clean
from jaypore_ci.interfaces import Executor, TriggerFailed, JobStatus
from jaypore_ci.logging import logger
@@ -111,10 +112,7 @@ class Docker(Executor):
"""
Generates a clean job name slug.
"""
- name = "".join(
- l if l in "abcdefghijklmnopqrstuvwxyz1234567890" else "-"
- for l in job.name.lower()
- )
+ name = clean.name(job.name)
if tail:
return name
return f"jayporeci__job__{self.pipe_id}__{name}"
diff --git a/jaypore_ci/jci.py b/jaypore_ci/jci.py
@@ -12,7 +12,7 @@ from contextlib import contextmanager
import structlog
import pendulum
-from jaypore_ci import remotes, executors, reporters, repos
+from jaypore_ci import remotes, executors, reporters, repos, clean
from jaypore_ci.interfaces import (
Remote,
Executor,
@@ -126,10 +126,7 @@ class Job: # pylint: disable=too-many-instance-attributes
report = self.pipeline.reporter.render(self.pipeline)
with open("/jaypore_ci/run/jaypore_ci.status.txt", "w", encoding="utf-8") as fl:
fl.write(report)
- try:
- self.pipeline.remote.publish(report, status)
- except Exception as e: # pylint: disable=broad-except
- self.logging().exception(e)
+ self.pipeline.remote.publish(report, status)
return report
def trigger(self):
@@ -337,6 +334,8 @@ class Pipeline: # pylint: disable=too-many-instance-attributes
"""
depends_on = [] if depends_on is None else depends_on
depends_on = [depends_on] if isinstance(depends_on, str) else depends_on
+ name = clean.name(name)
+ assert name, "Name should have some value after it is cleaned"
assert name not in self.jobs, f"{name} already defined"
assert name not in self.stages, "Stage name cannot match a job's name"
kwargs, job_kwargs = dict(self.pipe_kwargs), kwargs
@@ -434,6 +433,8 @@ class Pipeline: # pylint: disable=too-many-instance-attributes
Any kwargs passed to this stage are supplied to jobs created within
this stage.
"""
+ name = clean.name(name)
+ assert name, "Name should have some value after it is cleaned"
assert name not in self.jobs, "Stage name cannot match a job's name"
assert name not in self.stages, "Stage names cannot be re-used"
self.stages.append(name)
diff --git a/pyproject.toml b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "jaypore_ci"
-version = "0.2.19"
+version = "0.2.20"
description = ""
authors = ["arjoonn sharma <arjoonn.94@gmail.com>"]
homepage = "https://www.jayporeci.in/"
diff --git a/tests/conftest.py b/tests/conftest.py
@@ -1,58 +1,14 @@
import os
-import json
import unittest
import pytest
import tests.subprocess_mock # pylint: disable=unused-import
import tests.docker_mock # pylint: disable=unused-import
-from tests.requests_mock import Mock
+from tests.requests_mock import add_gitea_mocks, add_github_mocks, Mock
from jaypore_ci import jci, executors, remotes, reporters, repos
-def add_gitea_mocks(gitea):
- ISSUE_ID = 1
- # --- create PR
- create_pr_url = f"{gitea.api}/repos/{gitea.owner}/{gitea.repo}/pulls"
- Mock.post(create_pr_url, body="", status=201)
- Mock.post(create_pr_url, body="issue_id:{ISSUE_ID}", status=409)
- # --- get existing body
- Mock.get(
- f"{gitea.api}/repos/{gitea.owner}/{gitea.repo}/pulls/{ISSUE_ID}",
- body=json.dumps({"body": "Previous body in PR description."}),
- content_type="application/json",
- )
- # --- update body
- Mock.patch(f"{gitea.api}/repos/{gitea.owner}/{gitea.repo}/pulls/{ISSUE_ID}")
- # --- set commit status
- Mock.post(f"{gitea.api}/repos/{gitea.owner}/{gitea.repo}/statuses/{gitea.sha}")
- Mock.gitea_added = True
-
-
-def add_github_mocks(github):
- ISSUE_ID = 1
- # --- create PR
- create_pr_url = f"{github.api}/repos/{github.owner}/{github.repo}/pulls"
- Mock.post(create_pr_url, body="", status=404)
- Mock.get(
- create_pr_url,
- body=json.dumps([{"number": ISSUE_ID}]),
- content_type="application/json",
- )
- Mock.post(create_pr_url, body="issue_id:{ISSUE_ID}", status=409)
- # --- get existing body
- Mock.get(
- f"{github.api}/repos/{github.owner}/{github.repo}/pulls/{ISSUE_ID}",
- body=json.dumps({"body": "Already existing body in PR description."}),
- content_type="application/json",
- )
- # --- update body
- Mock.patch(f"{github.api}/repos/{github.owner}/{github.repo}/pulls/{ISSUE_ID}")
- # --- set commit status
- Mock.post(f"{github.api}/repos/{github.owner}/{github.repo}/statuses/{github.sha}")
- Mock.github_added = True
-
-
def idfn(x):
name = []
for _, item in sorted(x.items()):
@@ -61,6 +17,22 @@ def idfn(x):
return str(name)
+def factory(*, repo, remote, executor, reporter):
+ "Return a new pipeline every time the builder function is called"
+
+ def build():
+ r = repo.from_env()
+ return jci.Pipeline(
+ poll_interval=0,
+ repo=r,
+ remote=remote.from_env(repo=r),
+ executor=executor(),
+ reporter=reporter(),
+ )
+
+ return build
+
+
@pytest.fixture(
scope="function",
params=list(
@@ -85,19 +57,18 @@ def pipeline(request):
os.environ["JAYPORE_EMAIL_ADDR"] = "fake@email.com"
os.environ["JAYPORE_EMAIL_PASSWORD"] = "fake_email_password"
os.environ["JAYPORE_EMAIL_TO"] = "fake.to@mymailmail.com"
- kwargs = {}
- kwargs["repo"] = request.param["repo"].from_env()
- # --- remote
- kwargs["remote"] = request.param["remote"].from_env(repo=kwargs["repo"])
+ builder = factory(
+ repo=request.param["repo"],
+ remote=request.param["remote"],
+ executor=request.param["executor"],
+ reporter=request.param["reporter"],
+ )
if request.param["remote"] == remotes.Gitea and not Mock.gitea_added:
- add_gitea_mocks(kwargs["remote"])
+ add_gitea_mocks(builder().remote)
if request.param["remote"] == remotes.Github and not Mock.github_added:
- add_github_mocks(kwargs["remote"])
- kwargs["executor"] = request.param["executor"]()
- kwargs["reporter"] = request.param["reporter"]()
- p = jci.Pipeline(poll_interval=0, **kwargs)
+ add_github_mocks(builder().remote)
if request.param["remote"] == remotes.Email:
with unittest.mock.patch("smtplib.SMTP_SSL", autospec=True):
- yield p
+ yield builder
else:
- yield p
+ yield builder
diff --git a/tests/requests_mock.py b/tests/requests_mock.py
@@ -1,3 +1,5 @@
+import json
+
from typing import NamedTuple
from collections import defaultdict
@@ -9,6 +11,13 @@ class MockResponse(NamedTuple):
body: str
content_type: str
+ def json(self):
+ return json.loads(self.body)
+
+ @property
+ def text(self):
+ return self.body
+
class Mock:
registry = defaultdict(list)
@@ -38,13 +47,57 @@ class Mock:
def handle(cls, method):
def handler(url, **_):
options = cls.registry[method, url]
- resp = options[cls.index[method, url]]
+ index = cls.index[method, url]
+ resp = options[index]
cls.index[method, url] = (cls.index[method, url] + 1) % len(options)
return resp
return handler
+def add_gitea_mocks(gitea):
+ ISSUE_ID = 1
+ # --- create PR
+ create_pr_url = f"{gitea.api}/repos/{gitea.owner}/{gitea.repo}/pulls"
+ Mock.post(create_pr_url, body="", status=201)
+ Mock.post(create_pr_url, body=f"issue_id:{ISSUE_ID}", status=409)
+ # --- get existing body
+ Mock.get(
+ f"{gitea.api}/repos/{gitea.owner}/{gitea.repo}/pulls/{ISSUE_ID}",
+ body=json.dumps({"body": "Previous body in PR description."}),
+ content_type="application/json",
+ )
+ # --- update body
+ Mock.patch(f"{gitea.api}/repos/{gitea.owner}/{gitea.repo}/pulls/{ISSUE_ID}")
+ # --- set commit status
+ Mock.post(f"{gitea.api}/repos/{gitea.owner}/{gitea.repo}/statuses/{gitea.sha}")
+ Mock.gitea_added = True
+
+
+def add_github_mocks(github):
+ ISSUE_ID = 1
+ # --- create PR
+ create_pr_url = f"{github.api}/repos/{github.owner}/{github.repo}/pulls"
+ Mock.post(create_pr_url, body="", status=404)
+ Mock.get(
+ create_pr_url,
+ body=json.dumps([{"number": ISSUE_ID}]),
+ content_type="application/json",
+ )
+ Mock.post(create_pr_url, body=f"issue_id:{ISSUE_ID}", status=409)
+ # --- get existing body
+ Mock.get(
+ f"{github.api}/repos/{github.owner}/{github.repo}/pulls/{ISSUE_ID}",
+ body=json.dumps({"body": "Already existing body in PR description."}),
+ content_type="application/json",
+ )
+ # --- update body
+ Mock.patch(f"{github.api}/repos/{github.owner}/{github.repo}/pulls/{ISSUE_ID}")
+ # --- set commit status
+ Mock.post(f"{github.api}/repos/{github.owner}/{github.repo}/statuses/{github.sha}")
+ Mock.github_added = True
+
+
requests.get = Mock.handle("get")
requests.post = Mock.handle("post")
requests.patch = Mock.handle("patch")
diff --git a/tests/subprocess_mock.py b/tests/subprocess_mock.py
@@ -12,6 +12,13 @@ def sha():
return hex(random.getrandbits(128))
+__rev_parse__ = sha()
+__hash_object__ = sha()
+__mktree = sha()
+__commit_tree = sha()
+__update_ref__ = sha()
+
+
def check_output(cmd, **_):
text = ""
# repos.git
@@ -22,7 +29,7 @@ def check_output(cmd, **_):
elif "git branch" in cmd and "grep" in cmd:
text = "subprocess_mock_fake_branch"
elif "rev-parse HEAD" in cmd:
- text = sha()
+ text = __rev_parse__
elif "git log -1" in cmd:
text = "some_fake_git_commit_message\nfrom_subprocess_mock"
# jci
@@ -30,13 +37,13 @@ def check_output(cmd, **_):
text = "fake_pipe_id_from_subprocess_mock"
# remotes.git
elif "git hash-object" in cmd:
- text = sha()
+ text = __hash_object__
elif "git mktree" in cmd:
- text = sha()
+ text = __mktree
elif "git commit-tree" in cmd:
- text = sha()
+ text = __commit_tree
elif "git update-ref" in cmd:
- text = sha()
+ text = __update_ref__
return text.encode()
diff --git a/tests/test_hypo_jci.py b/tests/test_hypo_jci.py
@@ -0,0 +1,11 @@
+from hypothesis import given, strategies as st, settings, HealthCheck
+
+from jaypore_ci.clean import allowed_alphabet
+
+
+@given(st.text(alphabet=allowed_alphabet, min_size=1))
+@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=500)
+def test_hypo_jobs(pipeline, name):
+ pipeline = pipeline()
+ with pipeline as p:
+ p.job(name, name)
diff --git a/tests/test_jaypore_ci.py b/tests/test_jaypore_ci.py
@@ -6,6 +6,7 @@ def test_sanity():
def test_simple_linear_jobs(pipeline):
+ pipeline = pipeline()
with pipeline as p:
p.job("lint", "x")
p.job("test", "x", depends_on=["lint"])
@@ -14,6 +15,7 @@ def test_simple_linear_jobs(pipeline):
def test_no_duplicate_names(pipeline):
+ pipeline = pipeline()
with pytest.raises(AssertionError):
with pipeline as p:
p.job("lint", "x")
@@ -21,6 +23,7 @@ def test_no_duplicate_names(pipeline):
def test_dependency_has_to_be_defined_before_child(pipeline):
+ pipeline = pipeline()
with pytest.raises(AssertionError):
with pipeline as p:
p.job("x", "x", depends_on=["y"])
@@ -28,6 +31,7 @@ def test_dependency_has_to_be_defined_before_child(pipeline):
def test_dependency_cannot_cross_stages(pipeline):
+ pipeline = pipeline()
with pytest.raises(AssertionError):
with pipeline as p:
with p.stage("stage1"):
@@ -37,6 +41,7 @@ def test_dependency_cannot_cross_stages(pipeline):
def test_duplicate_stages_not_allowed(pipeline):
+ pipeline = pipeline()
with pytest.raises(AssertionError):
with pipeline as p:
with p.stage("stage1"):
@@ -46,6 +51,7 @@ def test_duplicate_stages_not_allowed(pipeline):
def test_stage_and_job_cannot_have_same_name(pipeline):
+ pipeline = pipeline()
with pytest.raises(AssertionError):
with pipeline as p:
with p.stage("x"):
@@ -53,6 +59,7 @@ def test_stage_and_job_cannot_have_same_name(pipeline):
def test_cannot_define_duplicate_jobs(pipeline):
+ pipeline = pipeline()
with pytest.raises(AssertionError):
with pipeline as p:
p.job("x", "x")
@@ -60,12 +67,14 @@ def test_cannot_define_duplicate_jobs(pipeline):
def test_non_service_jobs_must_have_commands(pipeline):
+ pipeline = pipeline()
with pytest.raises(AssertionError):
with pipeline as p:
p.job("x", None)
def test_call_chain_is_followed(pipeline):
+ pipeline = pipeline()
with pipeline as p:
for name in "pq":
p.job(name, name)
@@ -80,6 +89,7 @@ def test_call_chain_is_followed(pipeline):
def test_env_matrix_is_easy_to_make(pipeline):
+ pipeline = pipeline()
with pipeline as p:
for i, env in enumerate(p.env_matrix(A=[1, 2, 3], B=[5, 6, 7])):
p.job(f"job{i}", "fake command", env=env)