mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
dev_scripts: Add QA script
Add a script that makes the user go through the QA steps for a supported Dangerzone platform, and may optionally run them automatically, if the user agrees. Closes #287
This commit is contained in:
parent
feec73c60c
commit
14a7ca1ae5
2 changed files with 887 additions and 0 deletions
|
@ -94,6 +94,9 @@ jobs:
|
|||
- run:
|
||||
name: Run linters to enforce code style
|
||||
command: poetry run make lint
|
||||
- run:
|
||||
name: Check that the QA script is up to date with the docs
|
||||
command: ./dev_scripts/qa.py --check-refs
|
||||
|
||||
build-container-image:
|
||||
working_directory: /app
|
||||
|
|
884
dev_scripts/qa.py
Executable file
884
dev_scripts/qa.py
Executable file
|
@ -0,0 +1,884 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import abc
|
||||
import argparse
|
||||
import difflib
|
||||
import logging
|
||||
import re
|
||||
import selectors
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONTENT_QA = r"""## QA
|
||||
|
||||
To ensure that new releases do not introduce regressions, and support existing
|
||||
and newer platforms, we have to do the following:
|
||||
|
||||
- [ ] In `.circleci/config.yml`, add new platforms and remove obsolete platforms
|
||||
- [ ] Make sure that the tip of the `main` branch passes the CI tests.
|
||||
- [ ] Create a test build in Windows and make sure it works:
|
||||
- [ ] Create a new development environment with Poetry.
|
||||
- [ ] Build the container image and ensure the development environment uses
|
||||
the new image.
|
||||
- [ ] Run the Dangerzone tests.
|
||||
- [ ] Build and run the Dangerzone .exe
|
||||
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
|
||||
- [ ] Create a test build in macOS (Intel CPU) and make sure it works:
|
||||
- [ ] Create a new development environment with Poetry.
|
||||
- [ ] Build the container image and ensure the development environment uses
|
||||
the new image.
|
||||
- [ ] Run the Dangerzone tests.
|
||||
- [ ] Create and run an app bundle.
|
||||
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
|
||||
- [ ] Create a test build in macOS (M1/2 CPU) and make sure it works:
|
||||
- [ ] Grab the app bundle that we built for macOS above and run it.
|
||||
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
|
||||
- [ ] Create a test build in the most recent Ubuntu LTS platform (Ubuntu 22.04
|
||||
as of writing this) and make sure it works:
|
||||
- [ ] Create a new development environment with Poetry.
|
||||
- [ ] Build the container image and ensure the development environment uses
|
||||
the new image.
|
||||
- [ ] Run the Dangerzone tests.
|
||||
- [ ] Create a .deb package and install it system-wide.
|
||||
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
|
||||
- [ ] Create a test build in the most recent Fedora platform (Fedora 37 as of
|
||||
writing this) and make sure it works:
|
||||
- [ ] Create a new development environment with Poetry.
|
||||
- [ ] Build the container image and ensure the development environment uses
|
||||
the new image.
|
||||
- [ ] Run the Dangerzone tests.
|
||||
- [ ] Create an .rpm package and install it system-wide.
|
||||
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
|
||||
- [ ] Run only the CI tests in the rest of the Linux platforms:
|
||||
- [ ] Create a new development environment with Poetry.
|
||||
- [ ] Build the container image and ensure the development environment uses
|
||||
the new image.
|
||||
- [ ] Run the Dangerzone tests.
|
||||
|
||||
"""
|
||||
|
||||
CONTENT_QA_SCENARIOS = r"""### Scenarios
|
||||
|
||||
#### 1. Dangerzone correctly identifies that Docker/Podman is not installed
|
||||
|
||||
_(Only for MacOS / Windows)_
|
||||
|
||||
Temporarily hide the Docker/Podman binaries, e.g., rename the `docker` /
|
||||
`podman` binaries to something else. Then run Dangerzone. Dangerzone should
|
||||
prompt the user to install Docker/Podman.
|
||||
|
||||
#### 2. Dangerzone correctly identifies that Docker is not running
|
||||
|
||||
_(Only for MacOS / Windows)_
|
||||
|
||||
Stop the Docker Desktop application. Then run Dangerzone. Dangerzone should
|
||||
prompt the user to start Docker Desktop.
|
||||
|
||||
#### 3. Dangerzone successfully installs the container image
|
||||
|
||||
Remove the Dangerzone container image from Docker/Podman. Then run Dangerzone.
|
||||
Danerzone should install the container image successfully.
|
||||
|
||||
#### 4. Dangerzone retains the settings of previous runs
|
||||
|
||||
Run Dangerzone and make some changes in the settings (e.g., change the OCR
|
||||
language, toggle whether to open the document after conversion, etc.). Restart
|
||||
Dangerzone. Dangerzone should show the settings that the user chose.
|
||||
|
||||
#### 5. Dangerzone reports failed conversions
|
||||
|
||||
Run Dangerzone and convert the `tests/test_docs/sample_bad_pdf.pdf` document.
|
||||
Dangerzone should fail gracefully, by reporting that the operation failed, and
|
||||
showing the last error message.
|
||||
|
||||
#### 6. Dangerzone succeeds in converting multiple documents
|
||||
|
||||
Run Dangerzone against a list of documents, and tick all options. Ensure that:
|
||||
* Conversions take place sequentially.
|
||||
* Attempting to close the window while converting asks the user if they want to
|
||||
abort the conversions.
|
||||
* Conversions are completed successfully.
|
||||
* Conversions show individual progress.
|
||||
* _(Only for Linux)_ The resulting files open with the PDF viewer of our choice.
|
||||
* OCR seems to have detected characters in the PDF files.
|
||||
* The resulting files have been saved with the proper suffix, in the proper
|
||||
location.
|
||||
* The original files have been saved in the `unsafe/` directory.
|
||||
|
||||
#### 7. Dangerzone CLI succeeds in converting multiple documents
|
||||
|
||||
_(Only for Windows and Linux)_
|
||||
|
||||
Run Dangerzone CLI against a list of documents. Ensure that conversions happen
|
||||
sequentially, are completed successfully, and we see their progress.
|
||||
|
||||
#### 8. Dangerzone can open a document for conversion via right-click -> "Open With"
|
||||
|
||||
_(Only for Windows and MacOS)_
|
||||
|
||||
Go to a directory with office documents, right-click on one, and click on "Open
|
||||
With". We should be able to open the file with Dangerzone, and then convert it.
|
||||
|
||||
#### 9. Updating Dangerzone handles external state correctly.
|
||||
|
||||
_(Applies to Linux/Windows/MacOS. For MacOS/Windows, it requires an installer
|
||||
for the new version)_
|
||||
|
||||
Install the previous version of Dangerzone system-wide. Open the Dangerzone
|
||||
application and enable some non-default settings. Close the Dangerzone
|
||||
application and get the container image for that version. For example
|
||||
|
||||
```
|
||||
$ podman images dangerzone.rocks/dangerzone:latest
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
dangerzone.rocks/dangerzone latest <image ID> <date> <size>
|
||||
```
|
||||
|
||||
_(use `docker` on Windows/MacOS)_
|
||||
|
||||
Install the new version of Dangerzone system-wide. Open the Dangerzone
|
||||
application and make sure that the previously enabled settings still show up.
|
||||
Also, ensure that Dangerzone reports that the new image has been installed, and
|
||||
verify that it's different from the old one by doing:
|
||||
|
||||
```
|
||||
$ podman images dangerzone.rocks/dangerzone:latest
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
dangerzone.rocks/dangerzone latest <different ID> <newer date> <different size>
|
||||
```
|
||||
"""
|
||||
|
||||
CONTENT_BUILD_DEBIAN_UBUNTU = r"""## Debian/Ubuntu
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```sh
|
||||
sudo apt install -y podman dh-python python3 python3-stdeb python3-pyside2.qtcore python3-pyside2.qtgui python3-pyside2.qtwidgets python3-appdirs python3-click python3-xdg python3-colorama
|
||||
```
|
||||
|
||||
Install poetry:
|
||||
|
||||
```sh
|
||||
python3 -m pip install poetry
|
||||
```
|
||||
|
||||
Change to the `dangerzone` folder, and install the poetry dependencies:
|
||||
|
||||
```
|
||||
poetry install
|
||||
```
|
||||
|
||||
Build the latest container:
|
||||
|
||||
```sh
|
||||
./install/linux/build-image.sh
|
||||
```
|
||||
|
||||
Run from source tree:
|
||||
|
||||
```sh
|
||||
# start a shell in the virtual environment
|
||||
poetry shell
|
||||
|
||||
# run the CLI
|
||||
./dev_scripts/dangerzone-cli --help
|
||||
|
||||
# run the GUI
|
||||
./dev_scripts/dangerzone
|
||||
```
|
||||
|
||||
Create a .deb:
|
||||
|
||||
```sh
|
||||
./install/linux/build-deb.py
|
||||
```
|
||||
"""
|
||||
|
||||
CONTENT_BUILD_FEDORA = r"""## Fedora
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```sh
|
||||
sudo dnf install -y rpm-build podman python3 python3-setuptools python3-pyside2 python3-appdirs python3-click python3-pyxdg python3-colorama
|
||||
```
|
||||
|
||||
Install poetry:
|
||||
|
||||
```sh
|
||||
python -m pip install poetry
|
||||
```
|
||||
|
||||
Change to the `dangerzone` folder, and install the poetry dependencies:
|
||||
|
||||
```
|
||||
poetry install
|
||||
```
|
||||
|
||||
Build the latest container:
|
||||
|
||||
```sh
|
||||
./install/linux/build-image.sh
|
||||
```
|
||||
|
||||
Run from source tree:
|
||||
|
||||
```sh
|
||||
# start a shell in the virtual environment
|
||||
poetry shell
|
||||
|
||||
# run the CLI
|
||||
./dev_scripts/dangerzone-cli --help
|
||||
|
||||
# run the GUI
|
||||
./dev_scripts/dangerzone
|
||||
```
|
||||
|
||||
Create a .rpm:
|
||||
|
||||
```sh
|
||||
./install/linux/build-rpm.py
|
||||
```
|
||||
"""
|
||||
|
||||
CONTENT_BUILD_WINDOWS = r"""## Windows
|
||||
|
||||
Install [Docker Desktop](https://www.docker.com/products/docker-desktop).
|
||||
|
||||
Install the latest version of Python 3.10 (64-bit) [from python.org](https://www.python.org/downloads/windows/). Make sure to check the "Add Python 3.10 to PATH" checkbox on the first page of the installer.
|
||||
|
||||
|
||||
Install Microsoft Visual C++ 14.0 or greater. Get it with ["Microsoft C++ Build Tools"](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and make sure to select "Desktop development with C++" when installing.
|
||||
|
||||
Install [poetry](https://python-poetry.org/). Open PowerShell, and run:
|
||||
|
||||
```
|
||||
python -m pip install poetry
|
||||
```
|
||||
|
||||
Change to the `dangerzone` folder, and install the poetry dependencies:
|
||||
|
||||
```
|
||||
poetry install
|
||||
```
|
||||
|
||||
Build the dangerzone container image:
|
||||
|
||||
```sh
|
||||
python .\install\windows\build-image.py
|
||||
```
|
||||
|
||||
After that you can launch dangerzone during development with:
|
||||
|
||||
```
|
||||
# start a shell in the virtual environment
|
||||
poetry shell
|
||||
|
||||
# run the CLI
|
||||
.\dev_scripts\dangerzone-cli.bat --help
|
||||
|
||||
# run the GUI
|
||||
.\dev_scripts\dangerzone.bat
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
class Reference:
|
||||
"""Reference a Markdown section in our docs.
|
||||
|
||||
This class holds a reference to a Markdown section in our docs, and compares it
|
||||
against to a stored version of this section in this file. The purpose of this class
|
||||
is to warn devs about changes in the docs that may affect the code as well. By
|
||||
having the stored doc section and the code in the same file, we limit the cases
|
||||
where those two may diverge.
|
||||
"""
|
||||
|
||||
REPO_URL = "https://github.com/freedomofpress/dangerzone"
|
||||
instances = []
|
||||
|
||||
def __init__(self, md_path, content):
|
||||
"""Initialize the class using the path for the docs and the cached section."""
|
||||
self.md_path = md_path
|
||||
self.content = content
|
||||
|
||||
# Figure out the heading of the section, and the GitHub markdown anchor, from
|
||||
# the content.
|
||||
first_line = self.content.split("\n", 1)[0]
|
||||
self.heading_title = re.sub(r"^\W+", "", first_line)
|
||||
self.md_anchor = self.get_md_anchor()
|
||||
|
||||
self.url = f"{self.REPO_URL}/blob/main/{self.md_path}#{self.md_anchor}"
|
||||
self.instances.append(self)
|
||||
|
||||
def ensure_up_to_date(self):
|
||||
"""Check if the referenced text has changed.
|
||||
|
||||
This is a consistency check to ensure that the specified QA instructions in this
|
||||
script are consistent with the ones described in the docs. If the check fails,
|
||||
this file needs to be updated.
|
||||
"""
|
||||
# Get section's text (as the file is now)
|
||||
with open(self.md_path, "r") as md_file:
|
||||
md_text_current = md_file.read()
|
||||
current_section_text = self.find_section_text(md_text_current)
|
||||
|
||||
# Check if there have been any changes from the cached section stored in this
|
||||
# file.
|
||||
if not self.content == current_section_text:
|
||||
logger.error(
|
||||
f"The contents of section '{self.heading_title}' in file"
|
||||
f" '{self.md_path}' have changed since this file was last updated!"
|
||||
)
|
||||
logger.error("Diff follows:")
|
||||
sys.stderr.writelines(self.diff(current_section_text))
|
||||
logger.error(
|
||||
"Please ensure the instructions in this script are up"
|
||||
" to date with the respective doc section, and then update the cached"
|
||||
" section in this file."
|
||||
)
|
||||
exit(1)
|
||||
|
||||
def find_section_text(self, md_text):
|
||||
"""Find a section's content in a provided Markdown string."""
|
||||
start = end = None
|
||||
orig_lines = md_text.splitlines(keepends=True)
|
||||
cur_lines = self.content.splitlines(keepends=True)
|
||||
for i, line in enumerate(orig_lines):
|
||||
if line == cur_lines[0]:
|
||||
break
|
||||
|
||||
start = i
|
||||
end = len(cur_lines) + i
|
||||
|
||||
# Ensure that no extra content has been added in that section, until a new
|
||||
# heading begins.
|
||||
for i, line in enumerate(orig_lines[end:]):
|
||||
if orig_lines[i] and orig_lines[i][0] == "#":
|
||||
break
|
||||
|
||||
end += i
|
||||
|
||||
return "".join(orig_lines[start:end])
|
||||
|
||||
def get_md_anchor(self):
|
||||
"""Attempt to get an anchor link to the markdown section on github.
|
||||
|
||||
Related: https://stackoverflow.com/questions/72536973/how-are-github-markdown-anchor-links-constructed
|
||||
"""
|
||||
# Remove '#' from header
|
||||
anchor = re.sub("^[#]+", "", self.heading_title)
|
||||
# Format
|
||||
anchor = anchor.strip().lower()
|
||||
# Convert spaces to dashes
|
||||
anchor = anchor.replace(" ", "-")
|
||||
# Remove non-alphanumeric (except dash and underscore)
|
||||
anchor = re.sub("[^a-zA-Z\-_]", "", anchor)
|
||||
|
||||
return anchor
|
||||
|
||||
def diff(self, source):
|
||||
"""Return a diff between the section in the docs and the stored one."""
|
||||
source = source.splitlines(keepends=True)
|
||||
content = self.content.splitlines(keepends=True)
|
||||
diff = difflib.unified_diff(
|
||||
source, content, fromfile=self.md_path, tofile="qa.py"
|
||||
)
|
||||
return diff
|
||||
|
||||
|
||||
class QABase(abc.ABC):
|
||||
"""Base class for the QA tasks."""
|
||||
|
||||
platforms = {}
|
||||
|
||||
REF_QA = Reference("RELEASE.md", content=CONTENT_QA)
|
||||
REF_QA_SCENARIOS = Reference("RELEASE.md", content=CONTENT_QA_SCENARIOS)
|
||||
|
||||
# The following class method is available since Python 3.6. For more details, see:
|
||||
# https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-simpler-customization-of-class-creation
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
# NOTE: Not all subclasses correspond to valid platforms. Some subclasses may
|
||||
# provide some basic functionality for an OS type or distro, but not a specific
|
||||
# version.
|
||||
if cls.get_id():
|
||||
cls.platforms[cls.get_id()] = cls
|
||||
|
||||
def __init__(self, try_auto=False, skip_manual=False, debug=False):
|
||||
self.debug = debug
|
||||
self.try_auto = try_auto
|
||||
self.skip_manual = skip_manual
|
||||
self.src = (
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"rev-parse",
|
||||
"--show-toplevel",
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
.stdout.decode()
|
||||
.strip("\n")
|
||||
)
|
||||
|
||||
def task(*msgs, ref=None, auto=False):
|
||||
"""Decorator for running a task automatically.
|
||||
|
||||
This decorator does the following:
|
||||
* Check if the user has asked to run the tasks automatically.
|
||||
- If not, ask the user if they want to try it out.
|
||||
- If yes, run the task (decorated function).
|
||||
* If an exception occurs, allow the user to skip the task, or stop the
|
||||
execution.
|
||||
|
||||
Args:
|
||||
msgs: A list of messages that we should show to the user, one message per
|
||||
line.
|
||||
ref: A link that can be used as a reference for a specific task.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
def inner(self, *args, **kwargs):
|
||||
# If the reference is actually defined in subclasses, grab it from
|
||||
# `self`.
|
||||
if isinstance(ref, str):
|
||||
_ref = getattr(self, ref)
|
||||
else:
|
||||
_ref = ref
|
||||
|
||||
# Assume that the first line is the description of the task, and
|
||||
# decorate it with some "=" signs.
|
||||
dec_msgs = list(msgs)
|
||||
num_equals = (80 - 2 - len(msgs[0])) // 2 or 3
|
||||
dec_msgs.insert(0, "")
|
||||
dec_msgs[1] = "=" * num_equals + " " + msgs[0] + " " + "=" * num_equals
|
||||
self.describe(*dec_msgs, ref=_ref)
|
||||
|
||||
# Handle manual tasks.
|
||||
if not auto:
|
||||
if self.skip_manual:
|
||||
logger.info("Skipping manual tasks, as instructed")
|
||||
return
|
||||
else:
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
# Detect if the task should run automatically, either because the user
|
||||
# has provided the `--try-auto` flag, or because they typed "auto" when
|
||||
# prompted.
|
||||
try_auto = False
|
||||
if self.try_auto:
|
||||
try_auto = True
|
||||
else:
|
||||
prompt = "Type '[a]uto' to try automatically, or '[s]kip' to skip: "
|
||||
choice = self.prompt(
|
||||
prompt=prompt, choices=["a", "auto", "s", "skip"]
|
||||
)
|
||||
try_auto = choice in ("a", "auto")
|
||||
|
||||
# Run the task automatically, if the user asked to, else assume that the
|
||||
# user has run it manually.
|
||||
if try_auto:
|
||||
logger.info("Trying automatically...")
|
||||
out = func(self, *args, **kwargs)
|
||||
logger.info("Completed successfully")
|
||||
return out
|
||||
|
||||
return inner
|
||||
|
||||
return decorator
|
||||
|
||||
def run(self, *args):
|
||||
"""Run a command with extra debug logs."""
|
||||
# Construct the command line representation of the provided arguments, by taking
|
||||
# the arguments and joining them with a space character. If an argument already
|
||||
# has a space character in it, quote it before adding it to the CLI
|
||||
# representation.
|
||||
cmd_str = ""
|
||||
for arg in args:
|
||||
if " " in arg:
|
||||
cmd_str += " '" + arg + "'"
|
||||
else:
|
||||
cmd_str += " " + arg
|
||||
|
||||
logger.debug("Running %s", cmd_str)
|
||||
try:
|
||||
subprocess.run(args, check=True)
|
||||
logger.debug("Successfully ran %s", cmd_str)
|
||||
except subprocess.SubprocessError:
|
||||
logger.debug("Failed to run %s", cmd_str)
|
||||
raise
|
||||
|
||||
def try_run(self, *args):
|
||||
"""Try to run a command and return True/False, depending on the result."""
|
||||
try:
|
||||
self.run(*args)
|
||||
return True
|
||||
except subprocess.SubprocessError:
|
||||
return False
|
||||
|
||||
def describe(self, *msgs, ref=None):
|
||||
"""Print a task's description.
|
||||
|
||||
Log one message per line, and optionally allow a URL as a reference.
|
||||
"""
|
||||
for msg in msgs:
|
||||
logger.info(msg)
|
||||
if ref:
|
||||
logger.info(
|
||||
f"For reference, see section '{ref.heading_title}' of '{ref.md_path}'"
|
||||
f" either locally, or in '{ref.url}'"
|
||||
)
|
||||
|
||||
def _consume_stdin(self):
|
||||
"""Consume the stdin of the process.
|
||||
|
||||
Consume the stdin of the process so that stray key presses from previous steps
|
||||
do not spill over.
|
||||
|
||||
It's imperative that we don't block in our attempt to consume the process'
|
||||
stdin. Typically, this requires setting the stdin file descriptor to
|
||||
non-blocking mode. However, this is not straight-forward on Windows.
|
||||
|
||||
Since we don't care about consuming the file descriptor *fast*, we can create a
|
||||
tight loop that checks if the file descriptor has data to read from, and then
|
||||
read a byte (else we risk blocking). Once the file descriptor has no data to
|
||||
read, we can return.
|
||||
"""
|
||||
sel = selectors.DefaultSelector()
|
||||
sel.register(sys.stdin, selectors.EVENT_READ)
|
||||
while sel.select(timeout=0):
|
||||
sys.stdin.read(1)
|
||||
|
||||
def prompt(self, *msgs, ref=None, prompt=None, choices=None):
|
||||
"""Print the task's description, and prompt the user for an action."""
|
||||
self.describe(*msgs, ref=ref)
|
||||
if prompt is None:
|
||||
if choices is None:
|
||||
prompt = "Press Enter once you completed this step to continue: "
|
||||
else:
|
||||
prompt = "Valid choices are %s: " % ", ".join(choices)
|
||||
|
||||
if self.skip_manual:
|
||||
logger.info("Skipping manual tasks, as instructed")
|
||||
return None
|
||||
else:
|
||||
self._consume_stdin()
|
||||
# If the dev has provided a list of valid choices, do not let the prompt
|
||||
# proceed until the user has provided one of those choices.
|
||||
while True:
|
||||
choice = input(prompt)
|
||||
if not choices or choice in choices:
|
||||
return choice
|
||||
|
||||
@task("Begin the QA scenarios", ref=REF_QA_SCENARIOS)
|
||||
def qa_scenarios(self, skip=None):
|
||||
"""Common prompt for the QA scenarios.
|
||||
|
||||
This method suggests to the user a way to check the QA scenarios, in a
|
||||
Dangerzone environment. Then, it iterates through them, prints their
|
||||
description, and asks the user if they pass.
|
||||
"""
|
||||
skip = skip or []
|
||||
self.describe(
|
||||
"You can execute into a test environment with:",
|
||||
"",
|
||||
f" cd {self.src}",
|
||||
f" ./dev_scripts/env.py --distro {self.DISTRO} --version"
|
||||
f" {self.VERSION} run -g bash",
|
||||
"",
|
||||
"and run either `dangerzone` or `dangerzone-cli`",
|
||||
)
|
||||
|
||||
scenarios = [
|
||||
s[5:] for s in CONTENT_QA_SCENARIOS.splitlines() if s.startswith("#### ")
|
||||
]
|
||||
|
||||
for num, scenario in enumerate(scenarios):
|
||||
self.describe(scenario)
|
||||
if num + 1 in skip:
|
||||
self.describe("Skipping for the current platform")
|
||||
else:
|
||||
self.prompt("Does it pass?", choices=["y", "n"])
|
||||
logger.info("Successfully completed QA scenarios")
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_id(cls):
|
||||
"""Get the identifier of this class for CLI usage."""
|
||||
raise NotImplementedError("The class has not specified an identifier")
|
||||
|
||||
@abc.abstractmethod
|
||||
def start(self):
|
||||
"""Start the QA tests for a platform."""
|
||||
raise NotImplementedError("No steps have been defined")
|
||||
|
||||
|
||||
# TODO: Test this class on Windows thorougly, and finish it.
|
||||
class QAWindows(QABase):
|
||||
"""Class for the Windows QA tasks."""
|
||||
|
||||
REF_BUILD = Reference("BUILD.md", content=CONTENT_BUILD_WINDOWS)
|
||||
|
||||
@QABase.task("Install and Run Docker Desktop", ref=REF_BUILD)
|
||||
def install_docker(self):
|
||||
logger.info("Checking if Docker Desktop is installed and running")
|
||||
if not self.try_run("docker", "info"):
|
||||
logger.info("Failed to verify that Docker Desktop is installed and running")
|
||||
self.prompt("Ensure that Docker Desktop is installed and running")
|
||||
else:
|
||||
logger.info("Verified that Docker Desktop is installed and running")
|
||||
|
||||
@QABase.task(
|
||||
"Install Poetry and the project's dependencies", ref=REF_BUILD, auto=True
|
||||
)
|
||||
def install_poetry(self):
|
||||
self.run("python", "-m", "pip", "install", "poetry")
|
||||
self.run("poetry", "install")
|
||||
|
||||
@QABase.task("Build Dangerzone container image", ref=REF_BUILD, auto=True)
|
||||
def build_image(self):
|
||||
self.run("python", r".\install\windows\build-image.py")
|
||||
|
||||
@classmethod
|
||||
def get_id(cls):
|
||||
return "windows"
|
||||
|
||||
def start(self):
|
||||
self.install_docker()
|
||||
self.install_poetry()
|
||||
self.build_image()
|
||||
|
||||
|
||||
class QALinux(QABase):
|
||||
"""Base class for all the Linux QA tasks."""
|
||||
|
||||
REF_BUILD = None
|
||||
DISTRO = None
|
||||
VERSION = None
|
||||
|
||||
def container_run(self, *args):
|
||||
"""Run a command inside a Dangerzone environment."""
|
||||
self.run(
|
||||
f"{self.src}/dev_scripts/env.py",
|
||||
"--distro",
|
||||
self.DISTRO,
|
||||
"--version",
|
||||
self.VERSION,
|
||||
"run",
|
||||
"--dev",
|
||||
*args,
|
||||
)
|
||||
|
||||
def shell_run(self, *args):
|
||||
"""Run a shell command inside a Dangerzone environment and source dir."""
|
||||
args_str = " ".join(args)
|
||||
self.container_run("bash", "-c", f"cd dangerzone; {args_str}")
|
||||
|
||||
def poetry_run(self, *args):
|
||||
"""Run a command via Poetry inside a Dangerzone environment."""
|
||||
self.shell_run("poetry", "run", *args)
|
||||
|
||||
@QABase.task(
|
||||
"Create Dangerzone build environment",
|
||||
"You can also run './dev_scripts/env.py ... build-dev'",
|
||||
ref="REF_BUILD",
|
||||
auto=True,
|
||||
)
|
||||
def build_dev_image(self):
|
||||
self.run(
|
||||
f"{self.src}/dev_scripts/env.py",
|
||||
"--distro",
|
||||
self.DISTRO,
|
||||
"--version",
|
||||
self.VERSION,
|
||||
"build-dev",
|
||||
)
|
||||
|
||||
@QABase.task("Build Dangerzone image", ref="REF_BUILD", auto=True)
|
||||
def build_container_image(self):
|
||||
self.shell_run("./install/linux/build-image.sh")
|
||||
# FIXME: We need to automate this part, simply by checking that the created
|
||||
# image is in `share/image-id.txt`.
|
||||
self.prompt("Ensure that the environment uses the created image")
|
||||
|
||||
@QABase.task("Run tests", ref="REF_BUILD", auto=True)
|
||||
def run_tests(self):
|
||||
self.poetry_run("make", "test")
|
||||
|
||||
def build_package(self):
|
||||
"""Build the Dangerzone .deb/.rpm package"""
|
||||
# Building a pakage is platform-specific, so subclasses must implement it.
|
||||
raise NotImplementedError("Building a package is not implemented")
|
||||
|
||||
@QABase.task("Create Dangerzone QA environment", ref="REF_BUILD", auto=True)
|
||||
def build_qa_image(self):
|
||||
self.run(
|
||||
f"{self.src}/dev_scripts/env.py",
|
||||
"--distro",
|
||||
self.DISTRO,
|
||||
"--version",
|
||||
self.VERSION,
|
||||
"build",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_id(cls):
|
||||
"""Return the ID for the QA class.
|
||||
|
||||
If the QA class is a base one, then either the distro or the version will be
|
||||
None. In this case, return None as well, to signify that this class does not
|
||||
correspond to a specific platform."""
|
||||
if not cls.DISTRO or not cls.VERSION:
|
||||
return None
|
||||
else:
|
||||
return f"{cls.DISTRO}-{cls.VERSION}"
|
||||
|
||||
def start(self):
|
||||
self.build_dev_image()
|
||||
self.build_container_image()
|
||||
self.run_tests()
|
||||
self.build_package()
|
||||
self.build_qa_image()
|
||||
self.qa_scenarios(skip=[1, 2, 8])
|
||||
|
||||
|
||||
class QADebianBased(QALinux):
|
||||
"""Base class for Debian-based distros.
|
||||
|
||||
This class simply points to the proper build instructions, and builds the Debian
|
||||
package.
|
||||
"""
|
||||
|
||||
REF_BUILD = Reference("BUILD.md", content=CONTENT_BUILD_DEBIAN_UBUNTU)
|
||||
|
||||
@QABase.task("Build .deb", ref=REF_BUILD, auto=True)
|
||||
def build_package(self):
|
||||
self.shell_run("./install/linux/build-deb.py")
|
||||
|
||||
|
||||
class QAUbuntu2204(QADebianBased):
|
||||
|
||||
DISTRO = "ubuntu"
|
||||
VERSION = "22.04"
|
||||
|
||||
|
||||
class QAUbuntu2210(QADebianBased):
|
||||
|
||||
DISTRO = "ubuntu"
|
||||
VERSION = "22.10"
|
||||
|
||||
|
||||
class QAFedora(QALinux):
|
||||
"""Base class for Fedora distros.
|
||||
|
||||
This class simply points to the proper build instructions, and builds the RPM
|
||||
package.
|
||||
"""
|
||||
|
||||
DISTRO = "fedora"
|
||||
REF_BUILD = Reference("BUILD.md", content=CONTENT_BUILD_FEDORA)
|
||||
|
||||
@QABase.task("Build .rpm", ref=REF_BUILD, auto=True)
|
||||
def build_package(self):
|
||||
self.container_run(
|
||||
"./dangerzone/install/linux/build-rpm.py",
|
||||
)
|
||||
|
||||
|
||||
class QAFedora36(QAFedora):
|
||||
|
||||
VERSION = "36"
|
||||
|
||||
|
||||
class QAFedora37(QAFedora):
|
||||
|
||||
VERSION = "37"
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=sys.argv[0],
|
||||
description="Run QA tests for a platform",
|
||||
)
|
||||
parser.add_argument(
|
||||
"platform",
|
||||
choices=sorted(QABase.platforms.keys()),
|
||||
help="Run QA tests for the provided platform",
|
||||
nargs="?",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--try-auto",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Try to run the automated parts of the QA steps",
|
||||
)
|
||||
# FIXME: Find a better way to skip tasks in non-interactive environments, while
|
||||
# retaining the ability to be semi-prompted in interactive environments.
|
||||
parser.add_argument(
|
||||
"--skip-manual",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Skip the manual parts of the QA steps",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Enable debug logs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-refs",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Check if references to docs still hold",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.check_refs and not args.platform:
|
||||
parser.print_help(sys.stderr)
|
||||
exit(1)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def setup_logging(debug=False):
|
||||
"""Simple way to setup logging.
|
||||
|
||||
Copied from: https://docs.python.org/3/howto/logging.html
|
||||
"""
|
||||
# specify level
|
||||
if debug:
|
||||
lvl = logging.DEBUG
|
||||
else:
|
||||
lvl = logging.INFO
|
||||
|
||||
logging.basicConfig(
|
||||
level=lvl,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
setup_logging(args.debug)
|
||||
|
||||
for ref in Reference.instances:
|
||||
ref.ensure_up_to_date()
|
||||
if args.check_refs:
|
||||
return
|
||||
|
||||
try:
|
||||
qa_cls = QABase.platforms[args.platform]
|
||||
except KeyError:
|
||||
raise RuntimeError("Unexpected platform: %s", args.platform)
|
||||
|
||||
logger.info("Starting QA tests for %s", args.platform.capitalize())
|
||||
qa_cls(args.try_auto, args.skip_manual, args.debug).start()
|
||||
logger.info("Successfully completed QA tests for %s", args.platform.capitalize())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
Loading…
Reference in a new issue