From 3fb24d477d76a55fe101abe2bf48b0364570d959 Mon Sep 17 00:00:00 2001 From: Gertjan Date: Thu, 3 Jul 2025 10:27:07 +0000 Subject: [PATCH 01/81] Update docker config and dev environment --- .devcontainer/devcontainer.json | 12 +- .vscode/launch.json | 6 +- .vscode/settings.json | 4 +- Dockerfile | 34 +- docker-compose.yml | 13 +- pyproject.toml | 122 +++- pyrightconfig.json | 9 - requirements.in | 11 - requirements.txt | 91 --- requirements_dev.in | 13 - requirements_dev.txt | 205 ------- uv.lock | 997 ++++++++++++++++++++++++++++++++ 12 files changed, 1147 insertions(+), 370 deletions(-) delete mode 100644 pyrightconfig.json delete mode 100644 requirements.in delete mode 100644 requirements.txt delete mode 100644 requirements_dev.in delete mode 100644 requirements_dev.txt create mode 100644 uv.lock diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 66862953..6100c690 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose { - "name": "Development", + "name": "bughog-devcontainer", // Update the 'dockerComposeFile' list if you have more compose files or use different names. // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. @@ -14,11 +14,18 @@ "service": "core_dev", "runServices": ["node_dev", "nginx_dev"], + "containerEnv": { + "UV_PROJECT_ENVIRONMENT": "/app/.venv" + }, + // The optional 'workspaceFolder' property is the path VS Code should open by default when // connected. This is typically a file mount in .devcontainer/docker-compose.yml "workspaceFolder": "/app", "customizations": { "vscode": { + "settings": { + "python.defaultInterpreterPath": "/app/.venv/bin/python" + }, "extensions": [ "charliermarsh.ruff", "ms-python.debugpy", @@ -28,8 +35,7 @@ } }, - // Install pip requirements - "postCreateCommand": "pip install -r requirements.txt" + "postCreateCommand": "uv sync --group dev" // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, diff --git a/.vscode/launch.json b/.vscode/launch.json index 855ea2f8..f8d0cfe3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,12 +8,14 @@ "name": "BugHog", "type": "debugpy", "request": "launch", - "program": "/app/bci/app.py", + "program": "${workspaceFolder}/bughog/app.py", + "python": "${workspaceFolder}/.venv/bin/python", + "cwd": "${workspaceFolder}", "purpose": [ "debug-test" ], "env": { - "PYTHONPATH": "/app", + "PYTHONPATH": "${workspaceFolder}", "DISPLAY": ":1", "MOZ_DISABLE_CONTENT_SANDBOX": "1", "PATH":"${PATH}:$HOME/.local/bin" diff --git a/.vscode/settings.json b/.vscode/settings.json index 1c443005..0cb6b213 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,5 @@ { - "python.defaultInterpreterPath": "/usr/local/bin/python", - "python.linting.enabled": true, - "python.linting.flake8Enabled": true, + "python.defaultInterpreterPath": "/app/.venv/bin/python", "python.testing.pytestArgs": [ "test" ], diff --git a/Dockerfile b/Dockerfile index 2a671954..114d26a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ FROM node:22.14-alpine as ui-build-stage WORKDIR /app -COPY /bci/web/vue/package*.json ./ +COPY /bughog/web/vue/package*.json ./ RUN npm install -COPY /bci/web/vue ./ +COPY /bughog/web/vue ./ RUN npm run build @@ -21,10 +21,11 @@ CMD ["start.sh"] FROM python:3.13-slim-bullseye AS base +COPY --from=ghcr.io/astral-sh/uv:0.7.15 /uv /uvx /bin/ WORKDIR /app RUN apt-get update -RUN apt install -y curl gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libnspr4 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils libgbm-dev xvfb dbus-x11 libnss3-tools python3-pip python3-tk python3-xlib gnome-screenshot vim git procps &&\ +RUN apt install -y curl gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libnspr4 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils libgbm-dev xvfb dbus-x11 libnss3-tools python3-tk python3-xlib gnome-screenshot vim git procps &&\ rm -rf /var/lib/apt/lists/* # Install Docker @@ -54,36 +55,31 @@ RUN curl -sSLo multiarch-support.deb http://security.debian.org/debian-security/ ln -s /usr/lib/x86_64-linux-gnu/libnspr4.so /usr/lib/x86_64-linux-gnu/libnspr4.so.0d RUN mkdir -p /app/logs && \ - mkdir -p /app/browser/binaries/chromium/downloaded && \ - mkdir -p /app/browser/binaries/firefox/downloaded && \ - mkdir -p /app/browser/binaries/chromium/artisanal && \ - mkdir -p /app/browser/binaries/firefox/artisanal + mkdir -p /app/subject/browser/binaries/chromium/downloaded && \ + mkdir -p /app/subject/browser/binaries/firefox/downloaded && \ + mkdir -p /app/subject/browser/binaries/chromium/artisanal && \ + mkdir -p /app/subject/browser/binaries/firefox/artisanal -COPY browser/profiles /app/browser/profiles +COPY subject/browser/profiles /app/subject/browser/profiles COPY --chmod=0755 scripts/ /app/scripts/ RUN cp /app/scripts/daemon/xvfb /etc/init.d/xvfb # Install python packages -COPY requirements.txt /app/requirements.txt -RUN pip install --user -r /app/requirements.txt +COPY pyproject.toml /app/ +RUN uv sync # Initiate PyAutoGUI RUN touch /root/.Xauthority && \ xauth add ${HOST}:0 . $(xxd -l 16 -p /dev/urandom) + FROM base AS core # Copy rest of source code -COPY bci /app/bci +COPY bughog /app/bughog ENTRYPOINT [ "/app/scripts/boot/core.sh" ] FROM base AS worker -# Copy rest of source code -COPY bci /app/bci +# Copy rest of source codez +COPY bughog /app/bughog ENTRYPOINT [ "/app/scripts/boot/worker.sh" ] - - -FROM base AS dev -COPY requirements_dev.txt /app/requirements_dev.txt -RUN pip install --user -r requirements_dev.txt -ENTRYPOINT [ "/app/scripts/boot/core.sh" ] diff --git a/docker-compose.yml b/docker-compose.yml index f3457e2c..0e41a787 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,9 +44,6 @@ services: hostname: bh_core ports: - "5000:5000" - build: - context: . - target: dev image: bh_core_dev environment: - "DEVELOPMENT=1" @@ -138,11 +135,11 @@ services: aliases: - node volumes: - - ./bci/web/vue/public:/app/public:rw - - ./bci/web/vue/src:/app/src:rw - - ./bci/web/vue/postcss.config.js:/app/postcss.config.js:rw - - ./bci/web/vue/tailwind.config.js:/app/tailwind.config.js:rw - - ./bci/web/vue/vite.config.js:/app/vite.config.js:rw + - ./bughog/web/vue/public:/app/public:rw + - ./bughog/web/vue/src:/app/src:rw + - ./bughog/web/vue/postcss.config.js:/app/postcss.config.js:rw + - ./bughog/web/vue/tailwind.config.js:/app/tailwind.config.js:rw + - ./bughog/web/vue/vite.config.js:/app/vite.config.js:rw command: ["npm", "run", "dev", "--", "--host"] profiles: - dev diff --git a/pyproject.toml b/pyproject.toml index 87866e74..86f7aeb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,110 @@ -[tool.pytest.ini_options] -addopts = "--cov=bci --cov-report=xml --junitxml=junit.xml" +[project] +name = "bughog" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "blinker==1.9.0", + "certifi==2025.4.26", + "charset-normalizer==3.4.2", + "click==8.2.0", + "dnspython==2.7.0", + "docker==7.1.0", + "flask==3.1.1", + "flask-sock==0.7.0", + "flatten-dict==0.4.2", + "gunicorn==23.0.0", + "h11==0.16.0", + "idna==3.10", + "itsdangerous==2.2.0", + "jinja2==3.1.6", + "markupsafe==3.0.2", + "mouseinfo==0.1.3", + "packaging==25.0", + "pillow==11.2.1", + "pyautogui==0.9.54", + "pygetwindow==0.0.9", + "pymongo==4.12.1", + "pymsgbox==1.0.9", + "pyperclip==1.9.0", + "pyrect==0.2.0", + "pyscreeze==1.0.1", + "python3-xlib==0.15", + "pytweening==1.2.0", + "pyvirtualdisplay==3.0", + "requests==2.32.3", + "simple-websocket==1.1.0", + "six==1.17.0", + "urllib3==2.4.0", + "werkzeug==3.1.3", + "wsproto==1.2.0", + "xlib==0.21", +] +[dependency-groups] +dev = [ + "anybadge==1.16.0", + "astroid==3.3.10", + "autopep8==2.3.2", + "blinker==1.9.0", + "boto3==1.38.15", + "botocore==1.38.15", + "certifi==2025.4.26", + "charset-normalizer==3.4.2", + "click==8.2.0", + "coverage[toml]==7.8.0", + "debugpy==1.8.14", + "dill==0.4.0", + "dnspython==2.7.0", + "docker==7.1.0", + "flake8==7.2.0", + "flask==3.1.1", + "flask-sock==0.7.0", + "flatten-dict==0.4.2", + "gunicorn==23.0.0", + "h11==0.16.0", + "idna==3.10", + "iniconfig==2.1.0", + "isort==6.0.1", + "itsdangerous==2.2.0", + "jinja2==3.1.6", + "jmespath==1.0.1", + "markupsafe==3.0.2", + "mccabe==0.7.0", + "mouseinfo==0.1.3", + "packaging==25.0", + "pillow==11.2.1", + "platformdirs==4.3.8", + "pluggy==1.5.0", + "pyautogui==0.9.54", + "pycodestyle==2.13.0", + "pyflakes==3.3.2", + "pygetwindow==0.0.9", + "pylint==3.3.7", + "pymongo==4.12.1", + "pymsgbox==1.0.9", + "pyperclip==1.9.0", + "pyrect==0.2.0", + "pyscreeze==1.0.1", + "pytest==8.3.5", + "pytest-cov==6.1.1", + "pytest-flake8==1.3.0", + "python-dateutil==2.9.0.post0", + "python3-xlib==0.15", + "pytweening==1.2.0", + "pyvirtualdisplay==3.0", + "requests==2.32.3", + "s3transfer==0.12.0", + "simple-websocket==1.1.0", + "six==1.17.0", + "tomlkit==0.13.2", + "urllib3==2.4.0", + "werkzeug==3.1.3", + "wsproto==1.2.0", + "xlib==0.21", +] + +[tool.pytest.ini_options] +addopts = "--cov=bci --cov-report=xml --cov-report=term --junitxml=junit.xml" [tool.ruff] # Exclude a variety of commonly ignored directories. @@ -37,9 +141,6 @@ exclude = [ line-length = 120 indent-width = 4 -# Assume Python 3.8 -target-version = "py38" - [tool.ruff.lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or @@ -74,7 +175,7 @@ line-ending = "auto" # # This is currently disabled by default, but it is planned for this # to be opt-out in the future. -docstring-code-format = false +docstring-code-format = true # Set the line length limit used when formatting code snippets in # docstrings. @@ -82,3 +183,12 @@ docstring-code-format = false # This only has an effect when the `docstring-code-format` setting is # enabled. docstring-code-line-length = "dynamic" + +[tool.pyright] +exclude = [ + "**/.venv/**", + "**/build/**", + "**/dist/**", + "**/__pycache__/**", + "/app/subject/binaries/**", +] diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index 77930380..00000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "exclude": [ - "binaries", - "browser-repos", - "drivers", - "extensions", - "profiles" - ] -} \ No newline at end of file diff --git a/requirements.in b/requirements.in deleted file mode 100644 index f0993275..00000000 --- a/requirements.in +++ /dev/null @@ -1,11 +0,0 @@ -docker -Flask -flask-sock -flatten-dict -gunicorn -pymongo -requests -pyautogui -pyvirtualdisplay -Pillow -Xlib \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3d093ff2..00000000 --- a/requirements.txt +++ /dev/null @@ -1,91 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile requirements.in -# -blinker==1.9.0 - # via flask -certifi==2025.4.26 - # via requests -charset-normalizer==3.4.2 - # via requests -click==8.2.0 - # via flask -dnspython==2.7.0 - # via pymongo -docker==7.1.0 - # via -r requirements.in -flask==3.1.1 - # via - # -r requirements.in - # flask-sock -flask-sock==0.7.0 - # via -r requirements.in -flatten-dict==0.4.2 - # via -r requirements.in -gunicorn==23.0.0 - # via -r requirements.in -h11==0.16.0 - # via wsproto -idna==3.10 - # via requests -itsdangerous==2.2.0 - # via flask -jinja2==3.1.6 - # via flask -markupsafe==3.0.2 - # via - # flask - # jinja2 - # werkzeug -mouseinfo==0.1.3 - # via pyautogui -packaging==25.0 - # via gunicorn -pillow==11.2.1 - # via - # -r requirements.in - # pyscreeze -pyautogui==0.9.54 - # via -r requirements.in -pygetwindow==0.0.9 - # via pyautogui -pymongo==4.12.1 - # via -r requirements.in -pymsgbox==1.0.9 - # via pyautogui -pyperclip==1.9.0 - # via mouseinfo -pyrect==0.2.0 - # via pygetwindow -pyscreeze==1.0.1 - # via pyautogui -python3-xlib==0.15 - # via - # mouseinfo - # pyautogui -pytweening==1.2.0 - # via pyautogui -pyvirtualdisplay==3.0 - # via -r requirements.in -requests==2.32.3 - # via - # -r requirements.in - # docker -simple-websocket==1.1.0 - # via flask-sock -six==1.17.0 - # via - # flatten-dict - # xlib -urllib3==2.4.0 - # via - # docker - # requests -werkzeug==3.1.3 - # via flask -wsproto==1.2.0 - # via simple-websocket -xlib==0.21 - # via -r requirements.in diff --git a/requirements_dev.in b/requirements_dev.in deleted file mode 100644 index 891ddc36..00000000 --- a/requirements_dev.in +++ /dev/null @@ -1,13 +0,0 @@ --r requirements.txt -anybadge -autopep8 -boto3 -botocore -coverage -debugpy -flake8 -isort -pylint -pytest -pytest-cov -pytest-flake8 diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index b583036b..00000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,205 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile requirements_dev.in -# -anybadge==1.16.0 - # via -r requirements_dev.in -astroid==3.3.10 - # via pylint -autopep8==2.3.2 - # via -r requirements_dev.in -blinker==1.9.0 - # via - # -r requirements.txt - # flask -boto3==1.38.15 - # via -r requirements_dev.in -botocore==1.38.15 - # via - # -r requirements_dev.in - # boto3 - # s3transfer -certifi==2025.4.26 - # via - # -r requirements.txt - # requests -charset-normalizer==3.4.2 - # via - # -r requirements.txt - # requests -click==8.2.0 - # via - # -r requirements.txt - # flask -coverage[toml]==7.8.0 - # via - # -r requirements_dev.in - # pytest-cov -debugpy==1.8.14 - # via -r requirements_dev.in -dill==0.4.0 - # via pylint -dnspython==2.7.0 - # via - # -r requirements.txt - # pymongo -docker==7.1.0 - # via -r requirements.txt -flake8==7.2.0 - # via - # -r requirements_dev.in - # pytest-flake8 -flask==3.1.1 - # via - # -r requirements.txt - # flask-sock -flask-sock==0.7.0 - # via -r requirements.txt -flatten-dict==0.4.2 - # via -r requirements.txt -gunicorn==23.0.0 - # via -r requirements.txt -h11==0.16.0 - # via - # -r requirements.txt - # wsproto -idna==3.10 - # via - # -r requirements.txt - # requests -iniconfig==2.1.0 - # via pytest -isort==6.0.1 - # via - # -r requirements_dev.in - # pylint -itsdangerous==2.2.0 - # via - # -r requirements.txt - # flask -jinja2==3.1.6 - # via - # -r requirements.txt - # flask -jmespath==1.0.1 - # via - # boto3 - # botocore -markupsafe==3.0.2 - # via - # -r requirements.txt - # flask - # jinja2 - # werkzeug -mccabe==0.7.0 - # via - # flake8 - # pylint -mouseinfo==0.1.3 - # via - # -r requirements.txt - # pyautogui -packaging==25.0 - # via - # -r requirements.txt - # anybadge - # gunicorn - # pytest -pillow==11.2.1 - # via - # -r requirements.txt - # pyscreeze -platformdirs==4.3.8 - # via pylint -pluggy==1.5.0 - # via pytest -pyautogui==0.9.54 - # via -r requirements.txt -pycodestyle==2.13.0 - # via - # autopep8 - # flake8 -pyflakes==3.3.2 - # via flake8 -pygetwindow==0.0.9 - # via - # -r requirements.txt - # pyautogui -pylint==3.3.7 - # via -r requirements_dev.in -pymongo==4.12.1 - # via -r requirements.txt -pymsgbox==1.0.9 - # via - # -r requirements.txt - # pyautogui -pyperclip==1.9.0 - # via - # -r requirements.txt - # mouseinfo -pyrect==0.2.0 - # via - # -r requirements.txt - # pygetwindow -pyscreeze==1.0.1 - # via - # -r requirements.txt - # pyautogui -pytest==8.3.5 - # via - # -r requirements_dev.in - # pytest-cov - # pytest-flake8 -pytest-cov==6.1.1 - # via -r requirements_dev.in -pytest-flake8==1.3.0 - # via -r requirements_dev.in -python-dateutil==2.9.0.post0 - # via botocore -python3-xlib==0.15 - # via - # -r requirements.txt - # mouseinfo - # pyautogui -pytweening==1.2.0 - # via - # -r requirements.txt - # pyautogui -pyvirtualdisplay==3.0 - # via -r requirements.txt -requests==2.32.3 - # via - # -r requirements.txt - # docker -s3transfer==0.12.0 - # via boto3 -simple-websocket==1.1.0 - # via - # -r requirements.txt - # flask-sock -six==1.17.0 - # via - # -r requirements.txt - # flatten-dict - # python-dateutil - # xlib -tomlkit==0.13.2 - # via pylint -urllib3==2.4.0 - # via - # -r requirements.txt - # botocore - # docker - # requests -werkzeug==3.1.3 - # via - # -r requirements.txt - # flask -wsproto==1.2.0 - # via - # -r requirements.txt - # simple-websocket -xlib==0.21 - # via -r requirements.txt diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..b3baa839 --- /dev/null +++ b/uv.lock @@ -0,0 +1,997 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "anybadge" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/08/ddad0d5398d0961d506b0489e737a06e29a963eff0f2f0a2bb2cfb36dd1f/anybadge-1.16.0.tar.gz", hash = "sha256:f4e95eca834482f9932f9020ac2fe04a5ca863728b446324a8d35b1e67faab71", size = 34616, upload-time = "2025-01-11T23:03:27.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7d/01b2ac2fec808dea667b8678938156c3910219f2c45ee2e0b01e72786d72/anybadge-1.16.0-py3-none-any.whl", hash = "sha256:bc9ef2e20d875ee09237a15250a17b6fd7e67276f083d32a297963cdec179918", size = 28412, upload-time = "2025-01-11T23:03:24.857Z" }, +] + +[[package]] +name = "astroid" +version = "3.3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/c2/9b2de9ed027f9fe5734a6c0c0a601289d796b3caaf1e372e23fa88a73047/astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce", size = 398941, upload-time = "2025-05-10T13:33:10.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/58/5260205b9968c20b6457ed82f48f9e3d6edf2f1f95103161798b73aeccf0/astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb", size = 275388, upload-time = "2025-05-10T13:33:08.391Z" }, +] + +[[package]] +name = "autopep8" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycodestyle" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/d8/30873d2b7b57dee9263e53d142da044c4600a46f2d28374b3e38b023df16/autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758", size = 92210, upload-time = "2025-01-14T14:46:18.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "boto3" +version = "1.38.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/b5/f2cdcc4f909bd4a3b545571da9e7faa69a7ecc54bc5cc103f39dac53d115/boto3-1.38.15.tar.gz", hash = "sha256:dca60f7a3ead91c05cf471f7750e54d63b92bfc0e61fa122e2e3f5140e020a8d", size = 111836, upload-time = "2025-05-13T19:30:50.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/73/2b01ba5f08ef4fcb8e9b52d276be0747845a16aa712d420fd764cc498869/boto3-1.38.15-py3-none-any.whl", hash = "sha256:f2239b8c2816dd03a2a97297d33d5f7899f23be3a261db9a09ca691c32b7ddc2", size = 139936, upload-time = "2025-05-13T19:30:48.485Z" }, +] + +[[package]] +name = "botocore" +version = "1.38.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/35/0591b8cda1af4f308a0c073b838b02618547ae306504dc5a165a0ee88fd8/botocore-1.38.15.tar.gz", hash = "sha256:6adb3b1b0739153d5dc9758e5e06f7596290999c07ed8f9167ca62a1f97c6592", size = 13896855, upload-time = "2025-05-13T19:30:38.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3a/0025fc4de5440d11199efc75f3130e6f15750a9b898a506755da45838b56/botocore-1.38.15-py3-none-any.whl", hash = "sha256:9cc149ef56c3b99aff5436f26b71ad9558a45e4ac6b25b7b7046b0889279e601", size = 13557626, upload-time = "2025-05-13T19:30:33.964Z" }, +] + +[[package]] +name = "bughog" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "blinker" }, + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "click" }, + { name = "dnspython" }, + { name = "docker" }, + { name = "flask" }, + { name = "flask-sock" }, + { name = "flatten-dict" }, + { name = "gunicorn" }, + { name = "h11" }, + { name = "idna" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "mouseinfo" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyautogui" }, + { name = "pygetwindow" }, + { name = "pymongo" }, + { name = "pymsgbox" }, + { name = "pyperclip" }, + { name = "pyrect" }, + { name = "pyscreeze" }, + { name = "python3-xlib" }, + { name = "pytweening" }, + { name = "pyvirtualdisplay" }, + { name = "requests" }, + { name = "simple-websocket" }, + { name = "six" }, + { name = "urllib3" }, + { name = "werkzeug" }, + { name = "wsproto" }, + { name = "xlib" }, +] + +[package.dev-dependencies] +dev = [ + { name = "anybadge" }, + { name = "astroid" }, + { name = "autopep8" }, + { name = "blinker" }, + { name = "boto3" }, + { name = "botocore" }, + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "click" }, + { name = "coverage" }, + { name = "debugpy" }, + { name = "dill" }, + { name = "dnspython" }, + { name = "docker" }, + { name = "flake8" }, + { name = "flask" }, + { name = "flask-sock" }, + { name = "flatten-dict" }, + { name = "gunicorn" }, + { name = "h11" }, + { name = "idna" }, + { name = "iniconfig" }, + { name = "isort" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "jmespath" }, + { name = "markupsafe" }, + { name = "mccabe" }, + { name = "mouseinfo" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyautogui" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, + { name = "pygetwindow" }, + { name = "pylint" }, + { name = "pymongo" }, + { name = "pymsgbox" }, + { name = "pyperclip" }, + { name = "pyrect" }, + { name = "pyscreeze" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-flake8" }, + { name = "python-dateutil" }, + { name = "python3-xlib" }, + { name = "pytweening" }, + { name = "pyvirtualdisplay" }, + { name = "requests" }, + { name = "s3transfer" }, + { name = "simple-websocket" }, + { name = "six" }, + { name = "tomlkit" }, + { name = "urllib3" }, + { name = "werkzeug" }, + { name = "wsproto" }, + { name = "xlib" }, +] + +[package.metadata] +requires-dist = [ + { name = "blinker", specifier = "==1.9.0" }, + { name = "certifi", specifier = "==2025.4.26" }, + { name = "charset-normalizer", specifier = "==3.4.2" }, + { name = "click", specifier = "==8.2.0" }, + { name = "dnspython", specifier = "==2.7.0" }, + { name = "docker", specifier = "==7.1.0" }, + { name = "flask", specifier = "==3.1.1" }, + { name = "flask-sock", specifier = "==0.7.0" }, + { name = "flatten-dict", specifier = "==0.4.2" }, + { name = "gunicorn", specifier = "==23.0.0" }, + { name = "h11", specifier = "==0.16.0" }, + { name = "idna", specifier = "==3.10" }, + { name = "itsdangerous", specifier = "==2.2.0" }, + { name = "jinja2", specifier = "==3.1.6" }, + { name = "markupsafe", specifier = "==3.0.2" }, + { name = "mouseinfo", specifier = "==0.1.3" }, + { name = "packaging", specifier = "==25.0" }, + { name = "pillow", specifier = "==11.2.1" }, + { name = "pyautogui", specifier = "==0.9.54" }, + { name = "pygetwindow", specifier = "==0.0.9" }, + { name = "pymongo", specifier = "==4.12.1" }, + { name = "pymsgbox", specifier = "==1.0.9" }, + { name = "pyperclip", specifier = "==1.9.0" }, + { name = "pyrect", specifier = "==0.2.0" }, + { name = "pyscreeze", specifier = "==1.0.1" }, + { name = "python3-xlib", specifier = "==0.15" }, + { name = "pytweening", specifier = "==1.2.0" }, + { name = "pyvirtualdisplay", specifier = "==3.0" }, + { name = "requests", specifier = "==2.32.3" }, + { name = "simple-websocket", specifier = "==1.1.0" }, + { name = "six", specifier = "==1.17.0" }, + { name = "urllib3", specifier = "==2.4.0" }, + { name = "werkzeug", specifier = "==3.1.3" }, + { name = "wsproto", specifier = "==1.2.0" }, + { name = "xlib", specifier = "==0.21" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "anybadge", specifier = "==1.16.0" }, + { name = "astroid", specifier = "==3.3.10" }, + { name = "autopep8", specifier = "==2.3.2" }, + { name = "blinker", specifier = "==1.9.0" }, + { name = "boto3", specifier = "==1.38.15" }, + { name = "botocore", specifier = "==1.38.15" }, + { name = "certifi", specifier = "==2025.4.26" }, + { name = "charset-normalizer", specifier = "==3.4.2" }, + { name = "click", specifier = "==8.2.0" }, + { name = "coverage", extras = ["toml"], specifier = "==7.8.0" }, + { name = "debugpy", specifier = "==1.8.14" }, + { name = "dill", specifier = "==0.4.0" }, + { name = "dnspython", specifier = "==2.7.0" }, + { name = "docker", specifier = "==7.1.0" }, + { name = "flake8", specifier = "==7.2.0" }, + { name = "flask", specifier = "==3.1.1" }, + { name = "flask-sock", specifier = "==0.7.0" }, + { name = "flatten-dict", specifier = "==0.4.2" }, + { name = "gunicorn", specifier = "==23.0.0" }, + { name = "h11", specifier = "==0.16.0" }, + { name = "idna", specifier = "==3.10" }, + { name = "iniconfig", specifier = "==2.1.0" }, + { name = "isort", specifier = "==6.0.1" }, + { name = "itsdangerous", specifier = "==2.2.0" }, + { name = "jinja2", specifier = "==3.1.6" }, + { name = "jmespath", specifier = "==1.0.1" }, + { name = "markupsafe", specifier = "==3.0.2" }, + { name = "mccabe", specifier = "==0.7.0" }, + { name = "mouseinfo", specifier = "==0.1.3" }, + { name = "packaging", specifier = "==25.0" }, + { name = "pillow", specifier = "==11.2.1" }, + { name = "platformdirs", specifier = "==4.3.8" }, + { name = "pluggy", specifier = "==1.5.0" }, + { name = "pyautogui", specifier = "==0.9.54" }, + { name = "pycodestyle", specifier = "==2.13.0" }, + { name = "pyflakes", specifier = "==3.3.2" }, + { name = "pygetwindow", specifier = "==0.0.9" }, + { name = "pylint", specifier = "==3.3.7" }, + { name = "pymongo", specifier = "==4.12.1" }, + { name = "pymsgbox", specifier = "==1.0.9" }, + { name = "pyperclip", specifier = "==1.9.0" }, + { name = "pyrect", specifier = "==0.2.0" }, + { name = "pyscreeze", specifier = "==1.0.1" }, + { name = "pytest", specifier = "==8.3.5" }, + { name = "pytest-cov", specifier = "==6.1.1" }, + { name = "pytest-flake8", specifier = "==1.3.0" }, + { name = "python-dateutil", specifier = "==2.9.0.post0" }, + { name = "python3-xlib", specifier = "==0.15" }, + { name = "pytweening", specifier = "==1.2.0" }, + { name = "pyvirtualdisplay", specifier = "==3.0" }, + { name = "requests", specifier = "==2.32.3" }, + { name = "s3transfer", specifier = "==0.12.0" }, + { name = "simple-websocket", specifier = "==1.1.0" }, + { name = "six", specifier = "==1.17.0" }, + { name = "tomlkit", specifier = "==0.13.2" }, + { name = "urllib3", specifier = "==2.4.0" }, + { name = "werkzeug", specifier = "==3.1.3" }, + { name = "wsproto", specifier = "==1.2.0" }, + { name = "xlib", specifier = "==0.21" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444, upload-time = "2025-04-10T19:46:10.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676, upload-time = "2025-04-10T19:46:32.96Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514, upload-time = "2025-04-10T19:46:34.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756, upload-time = "2025-04-10T19:46:36.199Z" }, + { url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119, upload-time = "2025-04-10T19:46:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230, upload-time = "2025-04-10T19:46:54.077Z" }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + +[[package]] +name = "flake8" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/c4/5842fc9fc94584c455543540af62fd9900faade32511fab650e9891ec225/flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426", size = 48177, upload-time = "2025-03-29T20:08:39.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/5c/0627be4c9976d56b1217cb5187b7504e7fd7d3503f8bfd312a04077bd4f7/flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343", size = 57786, upload-time = "2025-03-29T20:08:37.902Z" }, +] + +[[package]] +name = "flask" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" }, +] + +[[package]] +name = "flask-sock" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/8f/c6ab717dc90f4e46d1430335cd4ab13e3629410bb760c0ead6de476760fb/flask-sock-0.7.0.tar.gz", hash = "sha256:e023b578284195a443b8d8bdb4469e6a6acf694b89aeb51315b1a34fcf427b7d", size = 4334, upload-time = "2023-10-02T22:32:42.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/98/107728ce3f430b5481eb426ccc5e1f7c8ab0bd01eaf231c62a8d528ff721/flask_sock-0.7.0-py3-none-any.whl", hash = "sha256:caac4d679392aaf010d02fabcf73d52019f5bdaf1c9c131ec5a428cb3491204a", size = 3982, upload-time = "2023-10-02T22:32:41.778Z" }, +] + +[[package]] +name = "flatten-dict" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/c6/5fe21639369f2ea609c964e20870b5c6c98a134ef12af848a7776ddbabe3/flatten-dict-0.4.2.tar.gz", hash = "sha256:506a96b6e6f805b81ae46a0f9f31290beb5fa79ded9d80dbe1b7fa236ab43076", size = 10362, upload-time = "2021-08-08T09:56:51.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/f5/ee39c6e92acc742c052f137b47c210cd0a1b72dcd3f98495528bb4d27761/flatten_dict-0.4.2-py2.py3-none-any.whl", hash = "sha256:7e245b20c4c718981212210eec4284a330c9f713e632e98765560e05421e48ad", size = 9656, upload-time = "2021-08-08T09:56:54.313Z" }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "isort" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mouseinfo" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyperclip" }, + { name = "python3-xlib", marker = "sys_platform == 'linux'" }, + { name = "rubicon-objc", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/fa/b2ba8229b9381e8f6381c1dcae6f4159a7f72349e414ed19cfbbd1817173/MouseInfo-0.1.3.tar.gz", hash = "sha256:2c62fb8885062b8e520a3cce0a297c657adcc08c60952eb05bc8256ef6f7f6e7", size = 10850, upload-time = "2020-03-27T21:20:10.136Z" } + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pyautogui" +version = "0.9.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mouseinfo" }, + { name = "pygetwindow" }, + { name = "pymsgbox" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "pyscreeze" }, + { name = "python3-xlib", marker = "sys_platform == 'linux'" }, + { name = "pytweening" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/ff/cdae0a8c2118a0de74b6cf4cbcdcaf8fd25857e6c3f205ce4b1794b27814/PyAutoGUI-0.9.54.tar.gz", hash = "sha256:dd1d29e8fd118941cb193f74df57e5c6ff8e9253b99c7b04f39cfc69f3ae04b2", size = 61236, upload-time = "2023-05-24T20:11:32.972Z" } + +[[package]] +name = "pycodestyle" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/6e/1f4a62078e4d95d82367f24e685aef3a672abfd27d1a868068fed4ed2254/pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae", size = 39312, upload-time = "2025-03-29T17:33:30.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/be/b00116df1bfb3e0bb5b45e29d604799f7b91dd861637e4d448b4e09e6a3e/pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", size = 31424, upload-time = "2025-03-29T17:33:29.405Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cc/1df338bd7ed1fa7c317081dcf29bf2f01266603b301e6858856d346a12b3/pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b", size = 64175, upload-time = "2025-03-31T13:21:20.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/40/b293a4fa769f3b02ab9e387c707c4cbdc34f073f945de0386107d4e669e6/pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a", size = 63164, upload-time = "2025-03-31T13:21:18.503Z" }, +] + +[[package]] +name = "pygetwindow" +version = "0.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyrect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/70/c7a4f46dbf06048c6d57d9489b8e0f9c4c3d36b7479f03c5ca97eaa2541d/PyGetWindow-0.0.9.tar.gz", hash = "sha256:17894355e7d2b305cd832d717708384017c1698a90ce24f6f7fbf0242dd0a688", size = 9699, upload-time = "2020-10-04T02:12:50.806Z" } + +[[package]] +name = "pylint" +version = "3.3.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/e4/83e487d3ddd64ab27749b66137b26dc0c5b5c161be680e6beffdc99070b3/pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559", size = 1520709, upload-time = "2025-05-04T17:07:51.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/83/bff755d09e31b5d25cc7fdc4bf3915d1a404e181f1abf0359af376845c24/pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d", size = 522565, upload-time = "2025-05-04T17:07:48.714Z" }, +] + +[[package]] +name = "pymongo" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/27/3634b2e8d88ad210ee6edac69259c698aefed4a79f0f7356cd625d5c423c/pymongo-4.12.1.tar.gz", hash = "sha256:8921bac7f98cccb593d76c4d8eaa1447e7d537ba9a2a202973e92372a05bd1eb", size = 2165515, upload-time = "2025-04-29T18:46:23.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/4d/e6654f3ec6819980cbad77795ccf2275cd65d6df41375a22cdbbccef8416/pymongo-4.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:90de2b060d69c22658ada162a5380a0f88cb8c0149023241b9e379732bd36152", size = 965051, upload-time = "2025-04-29T18:45:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/54/95/627a047c32789544a938abfd9311c914e622cb036ad16866e7e1b9b80239/pymongo-4.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edf4e05331ac875d3b27b4654b74d81e44607af4aa7d6bcd4a31801ca164e6fd", size = 964732, upload-time = "2025-04-29T18:45:19.478Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6d/7a604e3ab5399f8fe1ca88abdbf7e54ceb6cf03e64f68b2ed192d9a5eaf5/pymongo-4.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa7a817c9afb7b8775d98c469ddb3fe9c17daf53225394c1a74893cf45d3ade9", size = 1953037, upload-time = "2025-04-29T18:45:22.115Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d5/269388e7b0d02d35f55440baf1e0120320b6db1b555eaed7117d04b35402/pymongo-4.12.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9d142ca531694e9324b3c9ba86c0e905c5f857599c4018a386c4dc02ca490fa", size = 2030467, upload-time = "2025-04-29T18:45:24.069Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/04a6b48d6ca3fc2ff156185a3580799a748cf713239d6181e91234a663d3/pymongo-4.12.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5d4c0461f5cd84d9fe87d5a84b1bc16371c4dd64d56dcfe5e69b15c0545a5ac", size = 1994139, upload-time = "2025-04-29T18:45:26.215Z" }, + { url = "https://files.pythonhosted.org/packages/ad/65/0567052d52c0ac8aaa4baa700b39cdd1cf2481d2e59bd9817a3daf169ca0/pymongo-4.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43afd2f39182731ac9fb81bbc9439d539e4bd2eda72cdee829d2fa906a1c4d37", size = 1954947, upload-time = "2025-04-29T18:45:28.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5b/db25747b288218dbdd97e9aeff6a3bfa3f872efb4ed06fa8bec67b2a121e/pymongo-4.12.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:827ac668c003da7b175b8e5f521850e2c182b4638a3dec96d97f0866d5508a1e", size = 1904374, upload-time = "2025-04-29T18:45:30.943Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1e/6d0eb040c02ae655fafd63bd737e96d7e832eecfd0bd37074d0066f94a78/pymongo-4.12.1-cp313-cp313-win32.whl", hash = "sha256:7c2269b37f034124a245eaeb34ce031cee64610437bd597d4a883304babda3cd", size = 925869, upload-time = "2025-04-29T18:45:32.998Z" }, + { url = "https://files.pythonhosted.org/packages/59/b9/459da646d9750529f04e7e686f0cd8dd40174138826574885da334c01b16/pymongo-4.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:3b28ecd1305b89089be14f137ffbdf98a3b9f5c8dbbb2be4dec084f2813fbd5f", size = 948411, upload-time = "2025-04-29T18:45:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c3/75be116159f210811656ec615b2248f63f1bc9dd1ce641e18db2552160f0/pymongo-4.12.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f27b22a8215caff68bdf46b5b61ccd843a68334f2aa4658e8d5ecb5d3fbebb3b", size = 1021562, upload-time = "2025-04-29T18:45:37.433Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d1/2e8e368cad1c126a68365a6f53feaade58f9a16bd5f7a69f218af119b0e9/pymongo-4.12.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e9d23a3c290cf7409515466a7f11069b70e38ea2b786bbd7437bdc766c9e176", size = 1021553, upload-time = "2025-04-29T18:45:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/17/6e/a6460bc1e3d3f5f46cc151417427b2687a6f87972fd68a33961a37c114df/pymongo-4.12.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efeb430f7ca8649a6544a50caefead343d1fd096d04b6b6a002c6ce81148a85c", size = 2281736, upload-time = "2025-04-29T18:45:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e2/9e1d6f1a492bb02116074baa832716805a0552d757c176e7c5f40867ca80/pymongo-4.12.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a34e4a08bbcff56fdee86846afbc9ce751de95706ca189463e01bf5de3dd9927", size = 2368964, upload-time = "2025-04-29T18:45:43.579Z" }, + { url = "https://files.pythonhosted.org/packages/fa/df/88143016eca77e79e38cf072476c70dd360962934430447dabc9c6bef6df/pymongo-4.12.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b063344e0282537f05dbb11147591cbf58fc09211e24fc374749e343f880910a", size = 2327834, upload-time = "2025-04-29T18:45:45.847Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/df2998959b52cd5682b11e6eee1b0e0c104c07abd99c9cde5a871bb299fd/pymongo-4.12.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3f7941e01b3e5d4bfb3b4711425e809df8c471b92d1da8d6fab92c7e334a4cb", size = 2279126, upload-time = "2025-04-29T18:45:48.445Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3e/102636f5aaf97ccfa2a156c253a89f234856a0cd252fa602d4bf077ba3c0/pymongo-4.12.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41235014031739f32be37ff13992f51091dae9a5189d3bcc22a5bf81fd90dae", size = 2218136, upload-time = "2025-04-29T18:45:50.57Z" }, + { url = "https://files.pythonhosted.org/packages/44/c9/1b534c9d8d91d9d98310f2d955c5331fb522bd2a0105bd1fc31771d53758/pymongo-4.12.1-cp313-cp313t-win32.whl", hash = "sha256:9a1f07fe83a8a34651257179bd38d0f87bd9d90577fcca23364145c5e8ba1bc0", size = 974747, upload-time = "2025-04-29T18:45:52.66Z" }, + { url = "https://files.pythonhosted.org/packages/08/e2/7d3a30ac905c99ea93729e03d2bb3d16fec26a789e98407d61cb368ab4bb/pymongo-4.12.1-cp313-cp313t-win_amd64.whl", hash = "sha256:46d86cf91ee9609d0713242a1d99fa9e9c60b4315e1a067b9a9e769bedae629d", size = 1003332, upload-time = "2025-04-29T18:45:54.631Z" }, +] + +[[package]] +name = "pymsgbox" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ff/4c6f31a4f08979f12a663f2aeb6c8b765d3bd592e66eaaac445f547bb875/PyMsgBox-1.0.9.tar.gz", hash = "sha256:2194227de8bff7a3d6da541848705a155dcbb2a06ee120d9f280a1d7f51263ff", size = 18829, upload-time = "2020-10-11T01:51:43.227Z" } + +[[package]] +name = "pyobjc-core" +version = "11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/e9/0b85c81e2b441267bca707b5d89f56c2f02578ef8f3eafddf0e0c0b8848c/pyobjc_core-11.1.tar.gz", hash = "sha256:b63d4d90c5df7e762f34739b39cc55bc63dbcf9fb2fb3f2671e528488c7a87fe", size = 974602, upload-time = "2025-06-14T20:56:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/24/12e4e2dae5f85fd0c0b696404ed3374ea6ca398e7db886d4f1322eb30799/pyobjc_core-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:18986f83998fbd5d3f56d8a8428b2f3e0754fd15cef3ef786ca0d29619024f2c", size = 676431, upload-time = "2025-06-14T20:44:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/f7/79/031492497624de4c728f1857181b06ce8c56444db4d49418fa459cba217c/pyobjc_core-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8849e78cfe6595c4911fbba29683decfb0bf57a350aed8a43316976ba6f659d2", size = 719330, upload-time = "2025-06-14T20:44:51.621Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7d/6169f16a0c7ec15b9381f8bf33872baf912de2ef68d96c798ca4c6ee641f/pyobjc_core-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8cb9ed17a8d84a312a6e8b665dd22393d48336ea1d8277e7ad20c19a38edf731", size = 667203, upload-time = "2025-06-14T20:44:53.262Z" }, + { url = "https://files.pythonhosted.org/packages/49/0f/f5ab2b0e57430a3bec9a62b6153c0e79c05a30d77b564efdb9f9446eeac5/pyobjc_core-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:f2455683e807f8541f0d83fbba0f5d9a46128ab0d5cc83ea208f0bec759b7f96", size = 708807, upload-time = "2025-06-14T20:44:54.851Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/c5/7a866d24bc026f79239b74d05e2cf3088b03263da66d53d1b4cf5207f5ae/pyobjc_framework_cocoa-11.1.tar.gz", hash = "sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038", size = 5565335, upload-time = "2025-06-14T20:56:59.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/0b/a01477cde2a040f97e226f3e15e5ffd1268fcb6d1d664885a95ba592eca9/pyobjc_framework_cocoa-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:54e93e1d9b0fc41c032582a6f0834befe1d418d73893968f3f450281b11603da", size = 389049, upload-time = "2025-06-14T20:46:53.757Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/64cf2661f6ab7c124d0486ec6d1d01a9bb2838a0d2a46006457d8c5e6845/pyobjc_framework_cocoa-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd5245ee1997d93e78b72703be1289d75d88ff6490af94462b564892e9266350", size = 393110, upload-time = "2025-06-14T20:46:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/87/01e35c5a3c5bbdc93d5925366421e10835fcd7b23347b6c267df1b16d0b3/pyobjc_framework_cocoa-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:aede53a1afc5433e1e7d66568cc52acceeb171b0a6005407a42e8e82580b4fc0", size = 392644, upload-time = "2025-06-14T20:46:56.503Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7c/54afe9ffee547c41e1161691e72067a37ed27466ac71c089bfdcd07ca70d/pyobjc_framework_cocoa-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:1b5de4e1757bb65689d6dc1f8d8717de9ec8587eb0c4831c134f13aba29f9b71", size = 396742, upload-time = "2025-06-14T20:46:57.64Z" }, +] + +[[package]] +name = "pyobjc-framework-quartz" +version = "11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/ac/6308fec6c9ffeda9942fef72724f4094c6df4933560f512e63eac37ebd30/pyobjc_framework_quartz-11.1.tar.gz", hash = "sha256:a57f35ccfc22ad48c87c5932818e583777ff7276605fef6afad0ac0741169f75", size = 3953275, upload-time = "2025-06-14T20:58:17.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/27/4f4fc0e6a0652318c2844608dd7c41e49ba6006ee5fb60c7ae417c338357/pyobjc_framework_quartz-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43a1138280571bbf44df27a7eef519184b5c4183a588598ebaaeb887b9e73e76", size = 216816, upload-time = "2025-06-14T20:53:37.358Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8a/1d15e42496bef31246f7401aad1ebf0f9e11566ce0de41c18431715aafbc/pyobjc_framework_quartz-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b23d81c30c564adf6336e00b357f355b35aad10075dd7e837cfd52a9912863e5", size = 221941, upload-time = "2025-06-14T20:53:38.34Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/a3f84d06e567efc12c104799c7fd015f9bea272a75f799eda8b79e8163c6/pyobjc_framework_quartz-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:07cbda78b4a8fcf3a2d96e047a2ff01f44e3e1820f46f0f4b3b6d77ff6ece07c", size = 221312, upload-time = "2025-06-14T20:53:39.435Z" }, + { url = "https://files.pythonhosted.org/packages/76/ef/8c08d4f255bb3efe8806609d1f0b1ddd29684ab0f9ffb5e26d3ad7957b29/pyobjc_framework_quartz-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:39d02a3df4b5e3eee1e0da0fb150259476910d2a9aa638ab94153c24317a9561", size = 226353, upload-time = "2025-06-14T20:53:40.655Z" }, +] + +[[package]] +name = "pyperclip" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961, upload-time = "2024-06-18T20:38:48.401Z" } + +[[package]] +name = "pyrect" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/04/2ba023d5f771b645f7be0c281cdacdcd939fe13d1deb331fc5ed1a6b3a98/PyRect-0.2.0.tar.gz", hash = "sha256:f65155f6df9b929b67caffbd57c0947c5ae5449d3b580d178074bffb47a09b78", size = 17219, upload-time = "2022-03-16T04:45:52.36Z" } + +[[package]] +name = "pyscreeze" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/f0/cb456ac4f1a73723d5b866933b7986f02bacea27516629c00f8e7da94c2d/pyscreeze-1.0.1.tar.gz", hash = "sha256:cf1662710f1b46aa5ff229ee23f367da9e20af4a78e6e365bee973cad0ead4be", size = 27826, upload-time = "2024-08-20T23:03:07.291Z" } + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, +] + +[[package]] +name = "pytest-flake8" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/83/3b0154ccd60191e24b75c99c5e7c6dcfb1d2fd81dd47528523b38fed6ac6/pytest_flake8-1.3.0.tar.gz", hash = "sha256:88fb35562ce32d915c6ba41ef0d5e1cfcdd8ff884a32b7d46aa99fc77a3d1fe6", size = 13340, upload-time = "2024-11-09T00:09:09.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ca/163e24b6d92ba3e92245a6a23e88b946c29ff5294b2f4bc24c7a6171a13d/pytest_flake8-1.3.0-py3-none-any.whl", hash = "sha256:de10517c59fce25c0a7abb2a2b2a9d0b0ceb59ff0add7fa8e654d613bb25e218", size = 5966, upload-time = "2024-11-09T00:09:08.227Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python3-xlib" +version = "0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c6/2c5999de3bb1533521f1101e8fe56fd9c266732f4d48011c7c69b29d12ae/python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8", size = 132828, upload-time = "2014-05-31T12:28:59.603Z" } + +[[package]] +name = "pytweening" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/0c/c16bc93ac2755bac0066a8ecbd2a2931a1735a6fffd99a2b9681c7e83e90/pytweening-1.2.0.tar.gz", hash = "sha256:243318b7736698066c5f362ec5c2b6434ecf4297c3c8e7caa8abfe6af4cac71b", size = 171241, upload-time = "2024-02-20T03:37:56.809Z" } + +[[package]] +name = "pyvirtualdisplay" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/9f/23e5a82987c26d225139948a224a93318d7a7c8b166d4dbe4de7426dc4e4/PyVirtualDisplay-3.0.tar.gz", hash = "sha256:09755bc3ceb6eb725fb07eca5425f43f2358d3bf08e00d2a9b792a1aedd16159", size = 18560, upload-time = "2022-02-13T07:57:05.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/eb/c3b8deb661cb3846db63288c99bbb39f217b7807fc8acb2fd058db41e2e6/PyVirtualDisplay-3.0-py3-none-any.whl", hash = "sha256:40d4b8dfe4b8de8552e28eb367647f311f88a130bf837fe910e7f180d5477f0e", size = 15258, upload-time = "2022-02-13T07:57:04.051Z" }, +] + +[[package]] +name = "pywin32" +version = "310" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384, upload-time = "2025-03-17T00:56:04.383Z" }, + { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039, upload-time = "2025-03-17T00:56:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "rubicon-objc" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/83/e57741dcf862a2581d53eccf8b11749c97f73d9754bbc538fb6c7b527da3/rubicon_objc-0.5.1.tar.gz", hash = "sha256:90bee9fc1de4515e17615e15648989b88bb8d4d2ffc8c7c52748272cd7f30a66", size = 174639, upload-time = "2025-06-03T06:33:50.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/0a/e451c3dbda38dd6abab1fd16c3b35623fc0635dffcbbf97f1acc55a58508/rubicon_objc-0.5.1-py3-none-any.whl", hash = "sha256:17092756241b8370231cfaad45ad6e8ce99534987f2acbc944d65df5bdf8f6cd", size = 63323, upload-time = "2025-06-03T06:33:48.863Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/9e/73b14aed38ee1f62cd30ab93cd0072dec7fb01f3033d116875ae3e7b8b44/s3transfer-0.12.0.tar.gz", hash = "sha256:8ac58bc1989a3fdb7c7f3ee0918a66b160d038a147c7b5db1500930a607e9a1c", size = 149178, upload-time = "2025-04-22T21:08:09.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/64/d2b49620039b82688aeebd510bd62ff4cdcdb86cbf650cc72ae42c5254a3/s3transfer-0.12.0-py3-none-any.whl", hash = "sha256:35b314d7d82865756edab59f7baebc6b477189e6ab4c53050e28c1de4d9cce18", size = 84773, upload-time = "2025-04-22T21:08:08.265Z" }, +] + +[[package]] +name = "simple-websocket" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885, upload-time = "2024-08-14T08:19:41.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload-time = "2024-08-14T08:19:40.05Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" }, +] + +[[package]] +name = "xlib" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/d4/6033a97f96fc7d7bb822dab52e2e3c9532256d7ce033fa9675734941b9ac/xlib-0.21.tar.gz", hash = "sha256:60b7cd5d90f5d5922d9ce27b61589c07d970796558d417461db7b66e366bc401", size = 146776, upload-time = "2018-01-02T09:39:40.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/00/321541273b0ed2167b36c82be9baeb0bdc8af1c11c1b01de9436b84b5eaf/xlib-0.21-py2.py3-none-any.whl", hash = "sha256:8eee67dad83ef4b82bbbfa85d51eeb20c79d12b119fe25aa1d27bd602ff82212", size = 123811, upload-time = "2018-01-02T09:39:33.518Z" }, +] From 660e5295da5565b48b4752cc80c247c6438f0c75 Mon Sep 17 00:00:00 2001 From: Gertjan Date: Tue, 8 Jul 2025 19:13:46 +0000 Subject: [PATCH 02/81] Intermediary update --- Dockerfile | 12 +- bci/analysis/plot_factory.py | 73 ---- bci/browser/automation/terminal.py | 41 -- bci/browser/binary/vendors/chromium.py | 125 ------ bci/browser/binary/vendors/firefox.py | 107 ----- bci/browser/statistics.py | 39 -- bci/browser/support.py | 23 -- bci/database/mongo/revision_cache.py | 139 ------- bci/evaluations/collectors/collector.py | 42 -- bci/evaluations/custom/custom_evaluation.py | 276 ------------- bci/evaluations/evaluation_framework.py | 93 ----- bci/evaluations/experiments.py | 102 ----- bci/evaluations/logic.py | 341 ---------------- .../repository/online/chromium.py | 21 - .../repository/online/firefox.py | 21 - bci/version_control/state_result_factory.py | 64 --- bci/version_control/states/revisions/base.py | 101 ----- .../states/revisions/chromium.py | 53 --- .../states/revisions/firefox.py | 47 --- bci/version_control/states/state.py | 169 -------- .../states/versions/chromium.py | 40 -- .../states/versions/firefox.py | 30 -- bci/web/blueprints/api.py | 375 ------------------ bci/worker.py | 51 --- {bci => bughog}/__init__.py | 0 {bci => bughog}/analysis/__init__.py | 0 bughog/analysis/plot_factory.py | 60 +++ {bci => bughog}/app.py | 10 +- {bci => bughog}/cli.py | 0 {bci => bughog}/configuration.py | 14 +- {bci/browser => bughog/database}/__init__.py | 0 .../database/mongo}/__init__.py | 0 bughog/database/mongo/cache.py | 33 ++ {bci => bughog}/database/mongo/container.py | 2 +- .../database/mongo/executable_cache.py | 113 +++--- {bci => bughog}/database/mongo/mongodb.py | 295 +++++++------- .../distribution/worker_manager.py | 44 +- .../binary => bughog/evaluation}/__init__.py | 0 .../evaluation}/collectors/base.py | 10 + bughog/evaluation/collectors/collector.py | 34 ++ .../evaluation}/collectors/logs.py | 14 +- .../evaluation}/collectors/requests.py | 12 +- bughog/evaluation/custom/custom_evaluation.py | 220 ++++++++++ bughog/evaluation/evaluation.py | 174 ++++++++ bughog/evaluation/experiment.py | 277 +++++++++++++ bughog/evaluation/experiment_result.py | 32 ++ bughog/evaluation/file_structure.py | 57 +++ .../integration_tests}/__init__.py | 0 .../evaluation_configurations.py | 28 +- .../integration_tests/verify_results.py | 32 +- {bci => bughog}/main.py | 94 ++--- bughog/parameters.py | 329 +++++++++++++++ .../search_strategy}/__init__.py | 0 {bci => bughog}/search_strategy/bgb_search.py | 8 +- .../search_strategy/bgb_sequence.py | 6 +- .../search_strategy/composite_search.py | 10 +- .../search_strategy/sequence_strategy.py | 20 +- {bci/database => bughog/subject}/__init__.py | 0 bughog/subject/base.py | 62 +++ bughog/subject/evaluation_framework.py | 39 ++ bughog/subject/executable.py | 169 ++++++++ bughog/subject/factory.py | 95 +++++ bughog/subject/interaction.py | 22 + bughog/subject/state_oracle.py | 28 ++ .../subject/webbrowser}/__init__.py | 0 bughog/subject/webbrowser/base.py | 13 + .../subject/webbrowser/binary}/__init__.py | 0 .../webbrowser}/binary/artisanal_manager.py | 56 ++- .../subject/webbrowser}/binary/binary.py | 42 +- .../subject/webbrowser}/binary/factory.py | 8 +- .../webbrowser/binary/vendors/chromium.py | 69 ++++ .../webbrowser/binary/vendors/firefox.py | 58 +++ .../subject/webbrowser/chromium}/__init__.py | 0 .../subject/webbrowser/chromium/executable.py | 60 ++- bughog/subject/webbrowser/chromium/repo.py | 21 + .../webbrowser/chromium/state_oracle.py | 94 +++++ bughog/subject/webbrowser/chromium/subject.py | 76 ++++ .../webbrowser}/cli_options/chromium.py | 0 .../webbrowser}/cli_options/firefox.py | 0 .../webbrowser/configuration}/__init__.py | 0 .../webbrowser}/configuration/browser.py | 56 +-- .../webbrowser/configuration/chromium.py | 90 +++++ .../webbrowser}/configuration/firefox.py | 8 +- .../webbrowser}/configuration/options.py | 0 bughog/subject/webbrowser/evaluation.py | 25 ++ bughog/subject/webbrowser/executable.py | 31 ++ .../subject/webbrowser/firefox}/__init__.py | 0 .../subject/webbrowser/firefox/executable.py | 37 ++ bughog/subject/webbrowser/firefox/repo.py | 21 + .../webbrowser/firefox/state_oracle.py | 40 ++ bughog/subject/webbrowser/firefox/subject.py | 22 + .../webbrowser/interaction}/__init__.py | 0 .../webbrowser}/interaction/elements/five.png | Bin .../webbrowser}/interaction/elements/four.png | Bin .../webbrowser}/interaction/elements/one.png | Bin .../webbrowser}/interaction/elements/six.png | Bin .../interaction/elements/three.png | Bin .../webbrowser}/interaction/elements/two.png | Bin .../webbrowser}/interaction/interaction.py | 37 +- .../webbrowser}/interaction/simulation.py | 33 +- .../interaction/simulation_exception.py | 0 .../subject/webbrowser}/profile.py | 4 +- .../subject/webbrowser}/repository.py | 0 bughog/subject/webbrowser/state_cache.py | 136 +++++++ {bci => bughog}/util.py | 0 .../version_control}/__init__.py | 0 .../revision_parser/chromium_parser.py | 4 +- .../version_control/revision_parser/parser.py | 0 bughog/version_control/state/base.py | 168 ++++++++ bughog/version_control/state/commit/base.py | 96 +++++ .../version_control/state/commit/chromium.py | 18 + .../version_control/state/commit/firefox.py | 47 +++ .../version_control/state/release}/base.py | 26 +- .../version_control/state/release/chromium.py | 42 ++ .../version_control/state/release/firefox.py | 30 ++ .../version_control/state_factory.py | 45 +-- .../online => bughog/web}/__init__.py | 0 bughog/web/blueprints/api.py | 280 +++++++++++++ {bci => bughog}/web/blueprints/experiments.py | 2 +- {bci => bughog}/web/blueprints/test.py | 8 +- {bci => bughog}/web/clients.py | 31 +- {bci => bughog}/web/templates/base.html | 0 {bci => bughog}/web/templates/cookies.html | 0 {bci => bughog}/web/templates/experiment.html | 0 .../web/templates/integration_tests.html | 0 {bci => bughog}/web/vue/.gitignore | 0 {bci => bughog}/web/vue/index.html | 0 {bci => bughog}/web/vue/package-lock.json | 0 {bci => bughog}/web/vue/package.json | 0 {bci => bughog}/web/vue/postcss.config.js | 0 {bci => bughog}/web/vue/public/.gitkeep | 0 {bci => bughog}/web/vue/src/App.vue | 108 +++-- .../vue/src/components/evaluation_status.vue | 0 .../web/vue/src/components/gantt.vue | 0 .../web/vue/src/components/poc-editor.vue | 0 .../web/vue/src/components/section-header.vue | 28 +- .../web/vue/src/components/tooltip.vue | 0 .../web/vue/src/interaction_script_mode.js | 0 {bci => bughog}/web/vue/src/main.js | 0 {bci => bughog}/web/vue/src/style.css | 0 {bci => bughog}/web/vue/tailwind.config.js | 0 {bci => bughog}/web/vue/vite.config.js | 0 bughog/worker.py | 51 +++ experiments/README.md | 127 ------ .../pages/CSP/c320796/a.test/main/index.html | 8 - .../executable/chromium/artisanal/meta.json | 1 + .../executable/firefox/artisanal/Ladybird | Bin 0 -> 1578728 bytes .../executable/firefox/artisanal/meta.json | 1 + .../experiments/_default_files}/headers.json | 0 .../experiments/_default_files}/html | 0 .../webbrowser/experiments/_default_files}/js | 0 .../webbrowser/experiments/_default_files}/py | 0 .../experiments/_default_files}/script.cmd | 0 .../experiments/_default_files}/url_queue.txt | 0 .../experiments/webbrowser/Dockerfile | 0 .../all-zero/adition.com/main2}/headers.json | 0 .../all-zero/adition.com/main2/index.html | 19 + .../all-zero/leak.test/helper/headers.json | 6 + .../all-zero/leak.test/helper/index.js | 1 + .../all-zero/leak.test/main/headers.json | 6 + .../all-zero/leak.test/main/index.html | 19 + .../a.test/main/headers.json | 0 .../a.test/main/index.html | 3 + .../leak.test}/main/headers.json | 0 .../leak.test/main/index.html | 2 + .../cross-origin-frame-redirect/url_queue.txt | 2 + .../pages/BlackHat/form-blank/a.test/main.py | 15 + .../form-blank/leak.test/main/headers.json | 6 + .../form-blank/leak.test/main/index.html | 12 + .../pages/BlackHat/form-blank/script.cmd | 2 + .../leak.test/main/headers.json | 6 + .../leak.test/main/index.html | 12 + .../form-target-blank-fast-forward/script.cmd | 2 + .../leak.test/main/headers.json | 6 + .../leak.test/main/index.html | 14 + .../insecure-loads-local-embed/url_queue.txt | 2 + .../leak.test/main/headers.json | 6 + .../nonce-stealing/leak.test/main/index.html | 6 + .../picture/leak.test/main/headers.json | 1 + .../picture/leak.test/main/index.html | 13 + .../spoof-referer/leak.test/main/headers.json | 1 + .../spoof-referer/leak.test/main/index.html | 4 + .../webbrowser/pages/BlackHat/test/script.cmd | 12 + .../leak.test}/main/headers.json | 0 .../leak.test/main/index.html | 10 + .../upgrade-insecure-requests-link/script.cmd | 3 + .../CSP/c1001283/a.test/helper/headers.json | 0 .../CSP/c1001283/a.test/helper/index.html | 0 .../CSP/c1001283/leak.test/main/index.html | 0 .../pages/CSP/c1001283/url_queue.txt | 0 .../CSP/c1001982-img/a.test/main/headers.json | 0 .../CSP/c1001982-img/a.test/main/index.html | 0 .../c1001982-img/leak.test/helper/index.html | 0 .../c1001982-meta}/a.test/main/headers.json | 0 .../CSP/c1001982-meta/a.test/main/index.html | 12 + .../leak.test/helper/headers.json | 6 + .../leak.test/helper/index.html | 0 .../CSP/c1001982/a.test/main/headers.json | 0 .../pages/CSP/c1001982/a.test/main/index.html | 0 .../CSP/c1001982/leak.test/helper/index.html | 3 + .../CSP/c1064676/leak.test/helper/index.js | 0 .../CSP/c1064676/leak.test/main/headers.json | 0 .../CSP/c1064676/leak.test/main/index.html | 0 .../pages/CSP/c1064676/url_queue.txt | 0 .../c1072719-meta/leak.test/main/headers.json | 6 + .../c1072719-meta/leak.test/main/index.html | 13 + .../CSP/c1072719/leak.test/main/index.html | 0 .../pages/CSP/c1072719/url_queue.txt | 0 .../CSP/c1074317/a.test/helper/headers.json | 0 .../CSP/c1074317/a.test/helper/index.html | 0 .../CSP/c1074317/a.test/main/headers.json | 0 .../pages/CSP/c1074317/a.test/main/index.html | 0 .../pages/CSP/c1074317/a.test/script/index.js | 0 .../CSP/c1107824/a.test/helper/headers.json | 0 .../CSP/c1107824/a.test/helper/index.html | 0 .../CSP/c1107824/a.test/main/headers.json | 0 .../pages/CSP/c1107824/a.test/main/index.html | 0 .../pages/CSP/c1107824/url_queue.txt | 0 .../CSP/c1109167/a.test/main/headers.json | 0 .../pages/CSP/c1109167/a.test/main/index.html | 0 .../pages/CSP/c1109167/url_queue.txt | 0 .../c1115045-img/leak.test/main/headers.json | 0 .../c1115045-img/leak.test/main/index.html | 0 .../c1115045-meta/leak.test/main/headers.json | 6 + .../c1115045-meta/leak.test/main/index.html | 9 + .../CSP/c1115045/leak.test/main/headers.json | 0 .../CSP/c1115045/leak.test/main/index.html | 0 .../pages/CSP/c1115045/url_queue.txt | 0 .../CSP/c1115298-img/a.test/helper/index.html | 0 .../c1115298-img/leak.test/helper/index.js | 0 .../c1115298-img/leak.test/main/index.html | 0 .../CSP/c1115298/a.test/helper/index.html | 0 .../CSP/c1115298/leak.test/helper/index.js | 0 .../CSP/c1115298/leak.test/main/index.html | 0 .../pages/CSP/c1115298/url_queue.txt | 0 .../a.test/helper/headers.json | 0 .../a.test/helper/index.html | 0 .../a.test/secret/index.html | 0 .../leak.test/main/index.html | 0 .../CSP/c1115628-WebKitBlob/url_queue.txt | 0 .../c1115628-img/a.test/helper/headers.json | 0 .../CSP/c1115628-img/a.test/helper/index.html | 0 .../CSP/c1115628-img/a.test/secret/index.html | 0 .../c1115628-img/leak.test/main/index.html | 0 .../c1115628-meta/a.test/helper/headers.json | 6 + .../c1115628-meta/a.test/helper/index.html | 11 + .../c1115628-meta/leak.test/main/headers.json | 6 + .../c1115628-meta/leak.test/main/index.html | 5 + .../CSP/c1115628/a.test/helper/headers.json | 0 .../CSP/c1115628/a.test/helper/index.html | 0 .../CSP/c1115628/a.test/secret/index.html | 0 .../CSP/c1115628/leak.test/main/index.html | 0 .../pages/CSP/c1115628/url_queue.txt | 0 .../CSP/c1117687/a.test/main/headers.json | 0 .../pages/CSP/c1117687/a.test/main/index.html | 0 .../CSP/c1117687/a.test/secret/headers.json | 0 .../CSP/c1117687/a.test/secret/index.html | 0 .../CSP/c1117687/leak.test/main/index.html | 0 .../pages/CSP/c1117687/url_queue.txt | 0 .../CSP/c1180759/a.test/main/headers.json | 0 .../pages/CSP/c1180759/a.test/main/index.html | 0 .../pages/CSP/c1233067/a.test/main/index.html | 0 .../pages/CSP/c1233067/url_queue.txt | 0 .../CSP/c1248289/a.test/helper/headers.json | 0 .../pages/CSP/c1248289/a.test/helper/index.js | 0 .../CSP/c1248289/a.test/main/headers.json | 0 .../pages/CSP/c1248289/a.test/main/index.html | 0 .../pages/CSP/c1248289/url_queue.txt | 0 .../CSP/c1259077/a.test/helper/headers.json | 0 .../CSP/c1259077/a.test/helper/index.html | 0 .../CSP/c1259077/leak.test/main/headers.json | 0 .../CSP/c1259077/leak.test/main/index.html | 0 .../CSP/c1291482/leak.test/main/headers.json | 0 .../CSP/c1291482/leak.test/main/index.html | 0 .../CSP/c1329460/leak.test/main/headers.json | 0 .../CSP/c1329460/leak.test/main/index.html | 0 .../CSP/c1339146/leak.test/helper/index.js | 0 .../CSP/c1339146/leak.test/main/headers.json | 0 .../CSP/c1339146/leak.test/main/index.html | 0 .../CSP/c320796/a.test/main/headers.json | 0 .../pages/CSP/c320796/a.test/main/index.html | 13 + .../CSP/c358471/leak.test/helper/headers.json | 0 .../CSP/c358471/leak.test/helper/index.html | 0 .../CSP/c358471/leak.test/main/index.html | 0 .../CSP/c358471/leak.test/worker/headers.json | 0 .../CSP/c358471/leak.test/worker/index.js | 0 .../CSP/c377995/a.test/helper/headers.json | 0 .../CSP/c377995/a.test/helper/index.html | 0 .../pages/CSP/c377995/a.test/main/index.html | 0 .../pages/CSP/c377995/url_queue.txt | 0 .../CSP/c393401/leak.test/helper/index.html | 0 .../CSP/c393401/leak.test/main/index.html | 0 .../CSP/c393401/leak.test/script/index.html | 0 .../CSP/c411600/a.test/helper/headers.json | 0 .../CSP/c411600/a.test/helper/index.html | 0 .../pages/CSP/c411600/a.test/main/index.html | 0 .../leak.test/main/headers.json | 0 .../c482558-img-src/leak.test/main/index.html | 0 .../pages/CSP/c482558-img-src/url_queue.txt | 0 .../CSP/c482558/leak.test/main/headers.json | 0 .../CSP/c482558/leak.test/main/index.html | 0 .../pages/CSP/c482558/url_queue.txt | 0 .../CSP/c487155/a.test/helper1/index.html | 0 .../CSP/c487155/a.test/helper2/headers.json | 0 .../CSP/c487155/a.test/helper2/index.html | 0 .../pages/CSP/c487155/a.test/main/index.html | 0 .../c534542-iframe/a.test/main/headers.json | 0 .../CSP/c534542-iframe/a.test/main/index.html | 0 .../sub.a.test/helper/index.html | 0 .../CSP/c534542/a.test/main/headers.json | 0 .../pages/CSP/c534542/a.test/main/index.html | 0 .../CSP/c534542/sub.a.test/helper/index.js | 0 .../leak.test/main/index.html | 0 .../CSP/c560695/leak.test/main/index.html | 0 .../pages/CSP/c560695/url_queue.txt | 0 .../pages/CSP/c582387/a.test/helper/index.js | 0 .../CSP/c582387/a.test/main/headers.json | 0 .../pages/CSP/c582387/a.test/main/index.html | 0 .../pages/CSP/c582387/url_queue.txt | 0 .../c590505-iframe/a.test/main/headers.json | 0 .../CSP/c590505-iframe/a.test/main/index.html | 0 .../CSP/c590505-img/a.test/main/headers.json | 0 .../CSP/c590505-img/a.test/main/index.html | 0 .../CSP/c590505/a.test/main/headers.json | 0 .../pages/CSP/c590505/a.test/main/index.html | 0 .../CSP/c605451/leak.test/main/headers.json | 0 .../CSP/c605451/leak.test/main/index.html | 0 .../CSP/c610441/a.test/main/headers.json | 0 .../pages/CSP/c610441/a.test/main/index.html | 0 .../CSP/c630332/a.test/main/headers.json | 0 .../pages/CSP/c630332/a.test/main/index.html | 0 .../CSP/c633348/a.test/main/headers.json | 0 .../pages/CSP/c633348/a.test/main/index.html | 0 .../CSP/c633348/leak.test/helper/index.html | 0 .../CSP/c661126/a.test/main/headers.json | 0 .../pages/CSP/c661126/a.test/main/index.html | 0 .../a.test/helper/headers.json | 0 .../a.test/helper/index.html | 0 .../leak.test/main/headers.json | 0 .../leak.test/main/index.html | 0 .../CSP/c661852/a.test/helper/headers.json | 0 .../CSP/c661852/a.test/helper/index.html | 0 .../CSP/c661852/leak.test/main/headers.json | 0 .../CSP/c661852/leak.test/main/index.html | 0 .../CSP/c663620-img/a.test/main/headers.json | 0 .../CSP/c663620-img/a.test/main/index.html | 0 .../CSP/c663620/a.test/main/headers.json | 0 .../pages/CSP/c663620/a.test/main/index.html | 0 .../c669086-img/leak.test/main/headers.json | 0 .../CSP/c669086-img/leak.test/main/index.html | 0 .../leak.test/main/headers.json | 0 .../c669086-script/leak.test/main/index.html | 0 .../CSP/c669086/leak.test/main/headers.json | 0 .../CSP/c669086/leak.test/main/index.html | 0 .../c682673-script/a.test/main/headers.json | 0 .../CSP/c682673-script/a.test/main/index.html | 0 .../CSP/c682673/a.test/main/headers.json | 0 .../pages/CSP/c682673/a.test/main/index.html | 0 .../c689412-iframe/a.test/main/headers.json | 0 .../CSP/c689412-iframe/a.test/main/index.html | 0 .../CSP/c689412/a.test/main/headers.json | 0 .../pages/CSP/c689412/a.test/main/index.html | 0 .../CSP/c696806/a.test/main/headers.json | 0 .../pages/CSP/c696806/a.test/main/index.html | 0 .../CSP/c732779/a.test/helper/headers.json | 0 .../CSP/c732779/a.test/helper/index.html | 0 .../CSP/c732779/leak.test/main/index.html | 0 .../pages/CSP/c732779/url_queue.txt | 0 .../CSP/c740615/leak.test/main/index.html | 0 .../pages/CSP/c740615/url_queue.txt | 0 .../leak.test/main/headers.json | 0 .../c747847-iframe/leak.test/main/index.html | 0 .../c747847-img/leak.test/main/headers.json | 0 .../CSP/c747847-img/leak.test/main/index.html | 0 .../CSP/c767635/a.test/helper/index.html | 0 .../CSP/c767635/leak.test/main/index.html | 0 .../pages/CSP/c767635/url_queue.txt | 0 .../CSP/c777350/leak.test/main/headers.json | 0 .../CSP/c777350/leak.test/main/index.html | 0 .../pages/CSP/c777350/url_queue.txt | 0 .../leak.test/main/headers.json | 0 .../c799747-iframe/leak.test/main/index.html | 0 .../c799747-img/leak.test/main/headers.json | 0 .../CSP/c799747-img/leak.test/main/index.html | 0 .../leak.test/main/headers.json | 0 .../leak.test/main/index.html | 0 .../CSP/c799747-only-script-src/url_queue.txt | 0 .../leak.test/main/index.html | 0 .../CSP/c799747-x-webkit-csp/url_queue.txt | 0 .../CSP/c799747/leak.test/main/headers.json | 0 .../CSP/c799747/leak.test/main/index.html | 0 .../pages/CSP/c799747/url_queue.txt | 0 .../leak.test/main/headers.json | 0 .../leak.test/main/index.html | 0 .../CSP/c811691/leak.test/main/headers.json | 0 .../CSP/c811691/leak.test/main/index.html | 0 .../pages/CSP/c811691/url_queue.txt | 0 .../pages/CSP/c845961/a.test/main/index.html | 0 .../pages/CSP/c845961/url_queue.txt | 0 .../a.test/main/index.html | 0 .../leak.test/helper/index.html | 0 .../CSP/c894228-x-webkit-csp/url_queue.txt | 0 .../pages/CSP/c894228/a.test/main/index.html | 0 .../CSP/c894228/leak.test/helper/index.html | 0 .../CSP/c894228/leak.test/main/headers.json | 0 .../CSP/c894228/leak.test/main/index.html | 0 .../pages/CSP/c894228/url_queue.txt | 0 .../pages/CSP/c908207/a.test/helper/index.js | 0 .../CSP/c908207/a.test/main/headers.json | 0 .../pages/CSP/c908207/a.test/main/index.html | 0 .../CSP/c909865/a.test/helper/index.html | 0 .../pages/CSP/c909865/a.test/helper2/index.js | 0 .../CSP/c909865/a.test/main/headers.json | 0 .../pages/CSP/c909865/a.test/main/index.html | 0 .../CSP/c916326/a.test/helper/headers.json | 0 .../CSP/c916326/a.test/helper/index.html | 0 .../pages/CSP/c916326/a.test/helper2/index.js | 0 .../CSP/c916326/leak.test/main/index.html | 0 .../CSP/c932892/leak.test/helper/headers.json | 0 .../CSP/c932892/leak.test/helper/index.html | 0 .../CSP/c932892/leak.test/main/headers.json | 0 .../CSP/c932892/leak.test/main/index.html | 0 .../CSP/c941340/leak.test/main/headers.json | 0 .../CSP/c941340/leak.test/main/index.html | 0 .../pages/CSP/c941340/url_queue.txt | 0 .../c955350-img/leak.test/main/headers.json | 0 .../CSP/c955350-img/leak.test/main/index.html | 0 .../leak.test/main/headers.json | 0 .../c955350-script/leak.test/main/index.html | 0 .../CSP/c955350/leak.test/main/headers.json | 0 .../CSP/c955350/leak.test/main/index.html | 0 .../pages/CSP/c955350/url_queue.txt | 0 .../CSP/c957606-object/a.test/helper/index.js | 0 .../c957606-object/a.test/main/headers.json | 0 .../CSP/c957606-object/a.test/main/index.html | 0 .../pages/CSP/c957606/a.test/helper/index.js | 0 .../CSP/c957606/a.test/main/headers.json | 0 .../pages/CSP/c957606/a.test/main/index.html | 0 .../pages/CSP/c957606/url_queue.txt | 0 .../CSP/c967780/a.test/download/headers.json | 0 .../CSP/c967780/a.test/download/index.html | 0 .../pages/CSP/c967780/a.test/helper/index.js | 0 .../CSP/c967780/a.test/main/headers.json | 0 .../pages/CSP/c967780/a.test/main/index.html | 0 .../c971231-meta/leak.test/main/headers.json | 6 + .../c971231-meta/leak.test/main/index.html | 25 ++ .../leak.test/main/headers.json | 0 .../c971231-object/leak.test/main/index.html | 0 .../CSP/c971231/leak.test/main/headers.json | 0 .../CSP/c971231/leak.test/main/index.html | 0 .../pages/CSP/c971231/url_queue.txt | 0 .../leak.test/main/index.html | 0 .../CSP/c990581-x-webkit-csp/url_queue.txt | 0 .../CSP/c990581/leak.test/main/headers.json | 0 .../CSP/c990581/leak.test/main/index.html | 0 .../pages/CSP/c990581/url_queue.txt | 0 .../CSP/c992698/leak.test/main/headers.json | 0 .../CSP/c992698/leak.test/main/index.html | 0 .../pages/CSP/c992698/url_queue.txt | 0 .../CSP/f1007634/a.test/main/headers.json | 0 .../pages/CSP/f1007634/a.test/main/index.html | 0 .../pages/CSP/f1007634/a.test/worker/index.js | 0 .../pages/CSP/f1007634/url_queue.txt | 0 .../CSP/f1036399/a.test/main/headers.json | 0 .../pages/CSP/f1036399/a.test/main/index.html | 0 .../pages/CSP/f1036399/url_queue.txt | 0 .../f1073952-frame/a.test/main/headers.json | 0 .../CSP/f1073952-frame/a.test/main/index.html | 0 .../leak.test/main/headers.json | 0 .../f1073952-frame/leak.test/main/index.html | 0 .../CSP/f1073952-img/a.test/main/headers.json | 0 .../CSP/f1073952-img/a.test/main/index.html | 0 .../f1073952-img/leak.test/main/headers.json | 0 .../f1073952-img/leak.test/main/index.html | 0 .../f1073952-script/a.test/main/headers.json | 0 .../f1073952-script/a.test/main/index.html | 0 .../leak.test/main/headers.json | 0 .../f1073952-script/leak.test/main/index.html | 0 .../a.test/main/headers.json | 0 .../a.test/main/index.html | 0 .../leak.test/main/headers.json | 0 .../leak.test/main/index.html | 0 .../CSP/f1073952/a.test/main/headers.json | 0 .../pages/CSP/f1073952/a.test/main/index.html | 0 .../CSP/f1073952/leak.test/main/headers.json | 0 .../CSP/f1073952/leak.test/main/index.html | 0 .../pages/CSP/f1073952/url_queue.txt | 0 .../CSP/f1086999-img/a.test/main/headers.json | 0 .../CSP/f1086999-img/a.test/main/index.html | 0 .../f1086999-script/a.test/main/headers.json | 0 .../f1086999-script/a.test/main/index.html | 0 .../CSP/f1086999/a.test/main/headers.json | 0 .../pages/CSP/f1086999/a.test/main/index.html | 0 .../pages/CSP/f1086999/url_queue.txt | 0 .../a.test/main/headers.json | 0 .../a.test/main/index.html | 0 .../CSP/f1208559/a.test/main/headers.json | 0 .../pages/CSP/f1208559/a.test/main/index.html | 0 .../pages/CSP/f1208559/url_queue.txt | 0 .../CSP/f1223743/a.test/main/headers.json | 0 .../pages/CSP/f1223743/a.test/main/index.html | 0 .../pages/CSP/f1223743/url_queue.txt | 0 .../f1296976-meta/a.test/main/headers.json | 6 + .../CSP/f1296976-meta/a.test/main/index.html | 8 + .../CSP/f1296976/a.test/main/headers.json | 0 .../pages/CSP/f1296976/a.test/main/index.html | 0 .../pages/CSP/f1296976/url_queue.txt | 0 .../CSP/f1312272/a.test/main/headers.json | 0 .../pages/CSP/f1312272/a.test/main/index.html | 0 .../pages/CSP/f1312272/url_queue.txt | 0 .../CSP/f1316826/leak.test/main/headers.json | 0 .../CSP/f1316826/leak.test/main/index.html | 0 .../pages/CSP/f1316826/url_queue.txt | 0 .../leak.test/main/headers.json | 0 .../f1377426-frame/leak.test/main/index.html | 0 .../f1377426-img/leak.test/main/headers.json | 0 .../f1377426-img/leak.test/main/index.html | 0 .../CSP/f1377426/leak.test/main/headers.json | 0 .../CSP/f1377426/leak.test/main/index.html | 0 .../pages/CSP/f1377426/url_queue.txt | 0 .../CSP/f1396320/a.test/main/headers.json | 0 .../pages/CSP/f1396320/a.test/main/index.html | 0 .../pages/CSP/f1396320/url_queue.txt | 0 .../CSP/f1416045/a.test/main/headers.json | 0 .../pages/CSP/f1416045/a.test/main/index.html | 0 .../pages/CSP/f1416045/url_queue.txt | 0 .../CSP/f1422924/a.test/main/headers.json | 0 .../pages/CSP/f1422924/a.test/main/index.html | 0 .../pages/CSP/f1422924/url_queue.txt | 0 .../pages/CSP/f1432358/a.test/main/index.html | 0 .../pages/CSP/f1432358/url_queue.txt | 0 .../CSP/f1441468/leak.test/main/headers.json | 0 .../CSP/f1441468/leak.test/main/index.html | 0 .../pages/CSP/f1441468/url_queue.txt | 0 .../CSP/f1457100/a.test/main/headers.json | 0 .../pages/CSP/f1457100/a.test/main/index.html | 0 .../pages/CSP/f1457100/url_queue.txt | 0 .../a.test/main/headers.json | 0 .../f1460538-style-src/a.test/main/index.html | 0 .../CSP/f1460538/a.test/main/headers.json | 0 .../pages/CSP/f1460538/a.test/main/index.html | 0 .../pages/CSP/f1460538/url_queue.txt | 0 .../CSP/f1469150/a.test/helper/headers.json | 0 .../CSP/f1469150/a.test/helper/index.html | 0 .../CSP/f1469150/a.test/main/headers.json | 0 .../pages/CSP/f1469150/a.test/main/index.html | 0 .../pages/CSP/f1469150/url_queue.txt | 0 .../CSP/f1505412/a.test/helper/headers.json | 0 .../CSP/f1505412/a.test/helper/index.html | 0 .../CSP/f1505412/a.test/main/headers.json | 0 .../pages/CSP/f1505412/a.test/main/index.html | 0 .../pages/CSP/f1505412/url_queue.txt | 0 .../CSP/f1542194/a.test/helper/index.html | 0 .../CSP/f1542194/a.test/main/headers.json | 0 .../pages/CSP/f1542194/a.test/main/index.html | 0 .../CSP/f1542194/leak.test/helper2/index.html | 0 .../pages/CSP/f1548385/a.test/helper/index.js | 0 .../pages/CSP/f1548385/a.test/main/index.html | 0 .../pages/CSP/f1548385/url_queue.txt | 0 .../CSP/f1550414/a.test/helper/index.html | 0 .../CSP/f1550414/a.test/main/headers.json | 0 .../pages/CSP/f1550414/a.test/main/index.html | 0 .../pages/CSP/f1550414/url_queue.txt | 0 .../CSP/f1605814/leak.test/main/headers.json | 0 .../CSP/f1605814/leak.test/main/index.html | 0 .../pages/CSP/f1605814/url_queue.txt | 0 .../CSP/f1644076/a.test/helper/headers.json | 0 .../CSP/f1644076/a.test/helper/index.html | 0 .../CSP/f1644076/leak.test/main/index.html | 0 .../pages/CSP/f1644076/url_queue.txt | 0 .../CSP/f1644790/leak.test/main/headers.json | 0 .../CSP/f1644790/leak.test/main/index.html | 0 .../CSP/f1738418/leak.test/helper/index.html | 0 .../CSP/f1738418/leak.test/main/headers.json | 0 .../CSP/f1738418/leak.test/main/index.html | 0 .../CSP/f774136/a.test/main/headers.json | 0 .../pages/CSP/f774136/a.test/main/index.html | 0 .../CSP/f784158/a.test/main/headers.json | 0 .../pages/CSP/f784158/a.test/main/index.html | 0 .../CSP/f886164/a.test/helper/headers.json | 0 .../CSP/f886164/a.test/helper/index.html | 0 .../pages/CSP/f886164/a.test/main/index.html | 0 .../CSP/f908824/a.test/main/headers.json | 0 .../pages/CSP/f908824/a.test/main/index.html | 0 .../CSP/f908933/a.test/main/headers.json | 0 .../pages/CSP/f908933/a.test/main/index.html | 0 .../pages/CSP/f908933/url_queue.txt | 0 .../CSP/f910139/a.test/main/headers.json | 0 .../pages/CSP/f910139/a.test/main/index.xml | 0 .../CSP/f949706/leak.test/helper/index.css | 0 .../f949706/leak.test/helper2/headers.json | 0 .../CSP/f949706/leak.test/helper2/index.html | 0 .../CSP/f949706/leak.test/main/headers.json | 0 .../CSP/f949706/leak.test/main/index.html | 0 .../pages/CSP/f949706/url_queue.txt | 0 .../CSP/test8/sub.a.test/main/headers.json | 1 + .../CSP/test8/sub.a.test/main/index.html | 0 .../audio/leak.test/main/index.html | 158 ++++++++ .../audio/leak.test/script/headers.json | 6 + .../audio/leak.test/script/index.js | 148 +++++++ .../pages/Fingerprinting/audio/url_queue.txt | 2 + .../c50-64/leak.test/main/headers.json | 6 + .../c50-64/leak.test/main/index.html | 161 ++++++++ .../c65-76/leak.test/main/headers.json | 6 + .../c65-76/leak.test/main/index.html | 169 ++++++++ .../pages/Fingerprinting/c65-76/url_queue.txt | 1 + .../c77-85/leak.test/main/headers.json | 6 + .../c77-85/leak.test/main/index.html | 168 ++++++++ .../pages/Fingerprinting/c77-85/url_queue.txt | 1 + .../c86-90/leak.test/main/headers.json | 6 + .../c86-90/leak.test/main/index.html | 168 ++++++++ .../pages/Fingerprinting/c86-90/url_queue.txt | 1 + .../c91-/leak.test/main/headers.json | 6 + .../c91-/leak.test/main/index.html | 162 ++++++++ .../debug-c77/leak.test/main/headers.json | 6 + .../debug-c77/leak.test/main/index.html | 167 ++++++++ .../Fingerprinting/debug-c77/url_queue.txt | 2 + .../f2/leak.test/main/headers.json | 6 + .../f2/leak.test}/main/index.html | 0 .../f3/leak.test/main/headers.json | 6 + .../f3/leak.test/main/index.html | 162 ++++++++ .../fingerprint1/leak.test/main/headers.json | 6 + .../fingerprint1/leak.test/main/index.html | 162 ++++++++ .../simple/leak.test/main/headers.json | 6 + .../simple/leak.test/main/index.html | 11 + .../c1001283/a.test/main/headers.json | 6 + .../c1001283/a.test/main/index.html | 1 + .../c1001283/url_queue.txt | 2 + .../c1001982/leak.test/main/headers.json | 6 + .../c1001982/leak.test/main/index.html | 1 + .../c320796/a.test/main/headers.json | 6 + .../c320796/a.test/main/index.html | 6 + .../c358471/a.test/main/headers.json | 6 + .../c358471/a.test/main/index.html | 1 + .../c358471/a.test/worker/index.html | 3 + .../c377995/leak.test/main/headers.json | 6 + .../c377995/leak.test/main/index.html | 4 + .../c377995/url_queue.txt | 2 + .../c482558/a.test/main/headers.json | 6 + .../c482558/a.test/main/index.html | 3 + .../c534542/a.test/main/headers.json | 6 + .../c534542/a.test/main/index.html | 2 + .../c534542/url_queue.txt | 2 + .../c560695/a.test/index.html | 4 + .../c560695/a.test/manifest.json | 12 + .../c560695/leak.test/main/headers.json | 6 + .../c560695/leak.test/main/index.html | 7 + .../c560695/url_queue.txt | 2 + .../c582387/a.test/main/headers.json | 6 + .../c582387/a.test/main/index.html | 16 + .../c582387/url_queue.txt | 2 + .../c590505/a.test/main/headers.json | 6 + .../c590505/a.test/main/index.html | 5 + .../c590505/a.test/script/index.js | 1 + .../c605451/leak.test/main/headers.json | 6 + .../c605451/leak.test/main/index.html | 18 + .../c610441}/a.test/main/headers.json | 0 .../c610441/a.test/main/index.html | 8 + .../c630332/a.test/main/headers.json | 6 + .../c630332/a.test/main/index.html | 11 + .../c630332/url_queue.txt | 2 + .../c633348/a.test/main/headers.json | 6 + .../c633348/a.test/main/index.html | 15 + .../c633348/b.test/main/index.html | 8 + .../c661126/leak.test/main/headers.json | 6 + .../c661126/leak.test/main/index.html | 7 + .../c661126/url_queue.txt | 2 + .../c663620/a.test/main/headers.json | 6 + .../c663620/a.test/main/index.html | 5 + .../c663620/url_queue.txt | 2 + .../c669086/a.test/main/headers.json | 6 + .../c669086/a.test/main/index.html | 4 + .../c669086/url_queue.txt | 2 + .../c682673/a.test/main/headers.json | 4 + .../c682673/a.test/main/index.html | 13 + .../c682673/url_queue.txt | 2 + .../c689412/leak.test/main/headers.json | 6 + .../c689412/leak.test/main/index.html | 10 + .../c689412/url_queue.txt | 2 + .../c696806/leak.test/main/headers.json | 4 + .../c696806/leak.test/main/index.html | 4 + .../c696806/leak.test/manifest.txt | 9 + .../c696806/url_queue.txt | 2 + .../c732779/leak.test/main/headers.json | 4 + .../c732779/leak.test/main/index.html | 1 + .../c732779/url_queue.txt | 2 + .../c740615/a.test/main/headers.json | 6 + .../c740615/a.test/main/index.html | 9 + .../c740615/url_queue.txt | 2 + .../c767635/leak.test/main/headers.json | 6 + .../c767635/leak.test/main/index.html | 1 + .../c767635/url_queue.txt | 2 + .../c777350/leak.test/main/headers.json | 6 + .../c777350/leak.test/main/index.html | 14 + .../c777350/url_queue.txt | 2 + .../c799747/leak.test/main/headers.json | 6 + .../c799747/leak.test/main/index.html | 7 + .../c799747/url_queue.txt | 2 + .../c811691/leak.test/main/headers.json | 6 + .../c811691/leak.test/main/index.html | 4 + .../c845961/b.test/main/index.html | 2 + .../c845961/url_queue.txt | 2 + .../c894228/leak.test/main/headers.json | 6 + .../c894228/leak.test/main/index.html | 12 + .../c908207/a.test/main/headers.json | 6 + .../c908207/a.test/main/index.html | 9 + .../c908207/leak.test/xss.js | 1 + .../c908207/url_queue.txt | 2 + .../c909865/a.test/main/headers.json | 6 + .../c909865/a.test/main/index.html | 20 + .../c909865/a.test/test_frame/index.html | 2 + .../c909865/url_queue.txt | 2 + .../c916326/leak.test/main/headers.json | 6 + .../c916326/leak.test/main/index.html | 8 + .../c916326/url_queue.txt | 2 + .../c932892/a.test/main/index.html | 21 + .../c932892/url_queue.txt | 2 + .../c941340/a.test/main/headers.json | 6 + .../c941340/a.test/main/index.html | 3 + .../c941340/leak.test/module/index.js | 1 + .../c955350/leak.test/main/headers.json | 1 + .../c955350/leak.test/main/index.html | 2 + .../c955350/url_queue.txt | 2 + .../c957606/a.test/index/index.html | 18 + .../c957606/url_queue.txt | 2 + .../c967780/a.test/download/index.js | 6 + .../c967780/a.test/main/headers.json | 6 + .../c967780/a.test/main/index.html | 9 + .../c967780/server.py | 19 + .../c967780/url_queue.txt | 2 + .../c971231/a.test/main/index.html | 16 + .../c971231/url_queue.txt | 2 + .../c990581/a.test/main/headers.json | 1 + .../c990581/a.test/main/index.html | 16 + .../c992698/a.test/main/index.html | 14 + .../f1007634/a.test/main/headers.json | 6 + .../f1007634/a.test/main/index.html | 16 + .../f1007634/a.test/worker/index.js | 3 + .../f1036399/leak.test/main/headers.json | 10 + .../f1036399/leak.test/main/index.html | 9 + .../f1036399/url_queue.txt | 2 + .../f1312272/leak.test/main/headers.json | 6 + .../f1312272/leak.test/main/index.html | 2 + .../f1312272/url_queue.txt | 2 + .../f1316826/leak.test/main/headers.json | 4 + .../f1316826/leak.test/main/index.html | 2 + .../f1316826/url_queue.txt | 2 + .../f1377426/leak.test/main/headers.json | 4 + .../f1377426/leak.test/main/index.html | 11 + .../f1377426/url_queue.txt | 2 + .../f1396320/leak.test/helper/index.html | 1 + .../f1396320/leak.test/main/headers.json | 6 + .../f1396320/leak.test/main/index.html | 8 + .../f1396320/url_queue.txt | 2 + .../f1422924/a.test/main/index.html | 11 + .../f1422924/url_queue.txt | 2 + .../f1432358/a.test/main/index.html | 15 + .../f1432358/url_queue.txt | 2 + .../f1441468/leak.test/main/headers.json | 6 + .../f1441468/leak.test/main/index.html | 12 + .../f1457100/leak.test/main/index.html | 1 + .../f1457100/url_queue.txt | 2 + .../f1469150/leak.test/main/index.html | 14 + .../f1469150/url_queue.txt | 2 + .../f1505412/a.test/main/headers.json | 4 + .../f1505412/a.test/main/index.html | 13 + .../f1542194/leak.test/main/index.html | 31 ++ .../f1548385/a.test/main/headers.json | 6 + .../f1548385/a.test/main/index.html | 16 + .../f1548385/a.test/page/index.js | 2 + .../f1550414/a.test/main/index.html | 3 + .../f1550414/a.test/xs/index.html | 3 + .../f1550414/url_queue.txt | 2 + .../f1605814/a.test/main/headers.json | 6 + .../f1605814/a.test/main/index.html | 1 + .../f1605814/url_queue.txt | 2 + .../f1644076/leak.test/main/headers.json | 6 + .../f1644076/leak.test/main/index.html | 7 + .../f1644076/url_queue.txt | 2 + .../f1644790/a.test/main/headers.json | 6 + .../f1644790/a.test/main/index.html | 1 + .../f1644790/url_queue.txt | 2 + .../f1738418/a.test/child/index.html | 1 + .../f1738418/a.test/main/headers.json | 6 + .../f1738418/a.test/main/index.html | 11 + .../f1738418/url_queue.txt | 2 + .../f886164/leak.test/main/headers.json | 6 + .../f886164/leak.test/main/index.html | 4 + .../f949706/leak.test/main/index.html | 19 + .../f949706/leak.test/main/worker.js | 1 + .../leak.test/main/headers.json | 6 + .../leak.test/main/index.html | 18 + .../csp-overwrite/leak.test/main/headers.json | 6 + .../csp-overwrite/leak.test/main/index.html | 14 + .../leak.test/main/headers.json | 4 + .../nonce-stealing/leak.test/main/index.html | 6 + .../IndustryDay/nonce-stealing/script.cmd | 1 + .../leak.test/main/headers.json | 6 + .../leak.test/main/index.html | 9 + .../leak.test/main/headers.json | 6 + .../leak.test/main/index.html | 4 + .../all_reproduced/a.test/main/headers.json | 6 + .../all_reproduced/a.test/main/index.html | 0 .../click/a.test/main/headers.json | 6 + .../click/a.test/main/index.html | 0 .../pages/IntegrationTests/click/script.cmd | 0 .../none_reproduced/a.test/main/headers.json | 6 + .../none_reproduced/a.test/main/index.html | 10 + .../SOP/c323695987/a.test/main/headers.json | 6 + .../SOP/c323695987/a.test/main/index.html | 115 ++++++ .../SOP/c323695987/b.test/victim/headers.json | 6 + .../SOP/c323695987/b.test/victim/index.html | 5 + .../pages/SOP/c323695987/script.cmd | 11 + .../SOP/c40051153/a.test/main/headers.json | 6 + .../SOP/c40051153/a.test/main/index.html | 149 +++++++ .../webbrowser/pages/SOP/c40051153/script.cmd | 3 + .../SOP/c40051484/a.test/main/headers.json | 2 + .../SOP/c40051484/a.test/main/index.html | 1 + .../pages/SOP/c40051484/b.test/server.py | 18 + .../c40051484/b.test/set-cookie/headers.json | 6 + .../c40051484/b.test/set-cookie/index.html | 1 + .../webbrowser/pages/SOP/c40051484/script.cmd | 4 + .../SOP/c40052447/a.test/main/headers.json | 1 + .../SOP/c40052447/a.test/main/index.html | 186 +++++++++ .../webbrowser/pages/SOP/c40052447/script.cmd | 4 + .../a.test/main/headers.json | 6 + .../a.test/main/index.html | 208 ++++++++++ .../a.test/serviceworker/headers.json | 6 + .../a.test/serviceworker/index.js | 19 + .../pages/SOP/c40095912-transpiled/script.cmd | 5 + .../SOP/c40095912/a.test/main/headers.json | 6 + .../SOP/c40095912/a.test/main/index.html | 90 +++++ .../a.test/serviceworker/headers.json | 6 + .../c40095912/a.test/serviceworker/index.js | 15 + .../webbrowser/pages/SOP/c40095912/script.cmd | 5 + .../Support/AutoGUI/a.test/main/headers.json | 0 .../Support/AutoGUI/a.test/main/index.html | 0 .../pages/Support/AutoGUI/script.cmd | 0 .../Support/BTPC/leak.test/main/index.html | 0 .../pages/Support/BTPC/url_queue.txt | 0 .../leak.test/main/headers.json | 6 + .../leak.test/main/index.html | 13 + .../CSP-in-meta/leak.test/main/headers.json | 6 + .../CSP-in-meta/leak.test/main/index.html | 9 + .../Support/CSP/a.test/helper/headers.json | 0 .../Support/CSP/a.test/helper/index.html | 0 .../Support/CSP/leak.test/main/index.html | 0 .../pages/Support/CSP/url_queue.txt | 0 .../Downloading/a.test/main/headers.json | 6 + .../Downloading/a.test/main/index.html | 0 .../pages/Support/Downloading/script.cmd | 0 .../PythonServer/a.test/main/headers.json | 6 + .../PythonServer/a.test/main/index.html | 0 .../Support/PythonServer/a.test/server.py | 0 .../ServiceWorker/a.test/main/index.html | 0 .../pages/Support/ServiceWorker/url_queue.txt | 0 .../StatusCode/leak.test/main/headers.json | 10 + .../StatusCode/leak.test/main/index.html | 10 + .../c1074317/a.test/helper/headers.json | 10 + .../Support/c1074317/a.test/helper/index.html | 0 .../Support/c1074317/a.test/main/headers.json | 6 + .../Support/c1074317/a.test/main/index.html | 25 ++ .../Support/c1074317/a.test/script/index.js | 19 + .../Support/c740615/leak.test/main/index.html | 15 + .../pages/Support/down/url_queue.txt | 2 + .../form-action/a.test/main/headers.json | 0 .../form-action/a.test/main/index.html | 0 .../not-allow-modals/a.test/main/headers.json | 0 .../not-allow-modals/a.test/main/index.html | 0 .../leak.test/helper/index.js | 0 .../leak.test/main/headers.json | 0 .../leak.test/main/index.html | 0 .../Support/portal/a.test/main/headers.json | 6 + .../Support/portal/a.test/main/index.html | 10 + .../leak.test/main/headers.json | 0 .../leak.test/main/index.html | 0 .../strict-dynamic/leak.test/helper/index.js | 0 .../leak.test/main/headers.json | 0 .../strict-dynamic/leak.test/main/index.html | 0 .../a.test/main/headers.json | 6 + .../a.test/main/index.html | 0 .../experiments/webbrowser}/res/600x400.png | Bin .../webbrowser}/res/CSP/c756018.as | 0 .../webbrowser}/res/CSP/c756018.swf | Bin .../webbrowser}/res/big-gradient.jpg | Bin .../webbrowser}/res/black-transparent.png | Bin .../experiments/webbrowser}/res/btc.pdf | Bin .../experiments/webbrowser}/res/bughog.css | 0 .../experiments/webbrowser}/res/bughog.ico | Bin .../experiments/webbrowser}/res/bughog.js | 0 .../experiments/webbrowser}/res/bw.png | Bin .../experiments/webbrowser}/res/font.woff | Bin .../experiments/webbrowser/res/horse.mp3 | Bin 0 -> 28915 bytes .../experiments/webbrowser}/res/horse.ogg | Bin .../experiments/webbrowser}/res/rgb.png | Bin .../webbrowser}/res/short-text.txt | 0 .../experiments/webbrowser}/res/subtitles.vtt | 0 .../experiments/webbrowser}/res/video.webm | Bin .../webbrowser}/extensions/README.md | 0 .../webbrowser/extensions/chromium/.gitkeep | 0 .../webbrowser/extensions/firefox/.gitkeep | 0 .../chromium/17_btpc/Default/Preferences | 0 .../chromium/24_btpc/Default/Preferences | 0 .../chromium/36_btpc/Default/Preferences | 0 .../chromium/40_btpc/Default/Preferences | 0 .../chromium/46_btpc/Default/Preferences | 0 .../59_btpc/BrowserMetrics-active.pma | Bin .../chromium/59_btpc/Default/Preferences | 0 .../profiles/chromium/59_btpc/First Run | 0 .../profiles/chromium/59_btpc/Local State | 0 .../chromium/6_btpc/Default/Preferences | 0 .../webbrowser}/profiles/firefox/cert8.db | Bin .../profiles/firefox/default-67}/.parentlock | 0 .../firefox/default-67}/.startup-incomplete | 0 .../default-67/OfflineCache/index.sqlite | Bin .../firefox/default-67/addonStartup.json.lz4 | Bin .../profiles/firefox/default-67/addons.json | 0 .../profiles/firefox/default-67/blocklist.xml | 0 .../cache2/ce_T151c2VyQ29udGV4dElkPTUs | 0 .../cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= | 0 .../0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB | Bin .../1891807939636060566172C08C0AC5783A497AE0 | Bin .../287A739E5A799EFB74FF60A39CE892610F2EFA68 | Bin .../516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 | Bin .../5995C7CCB293DA761DBE3FB34BFE6BB474B55144 | Bin .../5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 | Bin .../60D49F42AAF00DA70E330093AB82F7A3CE4256C3 | Bin .../90942E3BB93C76DCD57A8404751979F24237E461 | Bin .../C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 | Bin .../CD029054D4F0EED4C7FCBE28737398A0390D94A1 | Bin .../CDF359E63200C01C1961DA51E2DC1A04CDBFB351 | Bin .../CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 | Bin .../D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 | Bin .../E21F074DBAD1CB7994F383C419228B689766FB1C | Bin .../profiles/firefox/default-67/cert9.db | Bin .../firefox/default-67/compatibility.ini | 0 .../firefox/default-67/content-prefs.sqlite | Bin .../firefox/default-67/cookies.sqlite | Bin .../firefox/default-67/cookies.sqlite-wal | Bin .../default-67/datareporting/state.json | 0 .../default-67/extension-preferences.json | 0 .../firefox/default-67/extensions.json | 0 .../firefox/default-67/favicons.sqlite | Bin .../firefox/default-67/favicons.sqlite-wal | Bin .../profiles/firefox/default-67/handlers.json | 0 .../profiles/firefox/default-67/key4.db | Bin .../firefox/default-67/permissions.sqlite | Bin .../profiles/firefox/default-67/pkcs11.txt | 0 .../profiles/firefox/default-67/places.sqlite | Bin .../firefox/default-67/places.sqlite-wal | Bin .../profiles/firefox/default-67/prefs.js | 0 .../base-track-digest256.pset | Bin .../base-track-digest256.sbstore | Bin .../content-track-digest256.pset | Bin .../content-track-digest256.sbstore | Bin .../mozplugin-block-digest256.pset | Bin .../mozplugin-block-digest256.sbstore | Bin .../mozstd-trackwhite-digest256.sbstore | Bin .../test-block-simple.pset | Bin .../test-block-simple.sbstore | Bin .../test-harmful-simple.pset | Bin .../test-harmful-simple.sbstore | Bin .../test-malware-simple.pset | Bin .../test-malware-simple.sbstore | Bin .../test-phish-simple.pset | Bin .../test-phish-simple.sbstore | Bin .../test-track-simple.pset | Bin .../test-track-simple.sbstore | Bin .../test-trackwhite-simple.pset | Bin .../test-trackwhite-simple.sbstore | Bin .../test-unwanted-simple.pset | Bin .../test-unwanted-simple.sbstore | Bin .../safebrowsing/test-block-simple.pset | Bin .../safebrowsing/test-block-simple.sbstore | Bin .../safebrowsing/test-harmful-simple.pset | Bin .../safebrowsing/test-harmful-simple.sbstore | Bin .../safebrowsing/test-malware-simple.pset | Bin .../safebrowsing/test-malware-simple.sbstore | Bin .../safebrowsing/test-phish-simple.pset | Bin .../safebrowsing/test-phish-simple.sbstore | Bin .../safebrowsing/test-track-simple.pset | Bin .../safebrowsing/test-track-simple.sbstore | Bin .../safebrowsing/test-trackwhite-simple.pset | Bin .../test-trackwhite-simple.sbstore | Bin .../safebrowsing/test-unwanted-simple.pset | Bin .../safebrowsing/test-unwanted-simple.sbstore | Bin .../firefox/default-67/search.json.mozlz4 | Bin .../default-67/sessionCheckpoints.json | 0 .../firefox/default-67/storage.sqlite | Bin .../.metadata | Bin .../.metadata-v2 | Bin .../3647222921wleabcEoxlt-eengsairo.sqlite | Bin ...3647222921wleabcEoxlt-eengsairo.sqlite-wal | 0 .../default-67/storage/ls-archive.sqlite | Bin .../storage/permanent/chrome/.metadata | Bin .../storage/permanent/chrome/.metadata-v2 | Bin .../1451318868ntouromlalnodry--epcr.sqlite | Bin ...1451318868ntouromlalnodry--epcr.sqlite-wal | 0 .../idb/1657114595AmcateirvtiSty.sqlite | Bin .../idb/1657114595AmcateirvtiSty.sqlite-wal | 0 .../chrome/idb/2918063365piupsah.sqlite | Bin .../chrome/idb/2918063365piupsah.sqlite-wal | 0 .../chrome/idb/3561288849sdhlie.sqlite | Bin .../chrome/idb/3561288849sdhlie.sqlite-wal | 0 .../idb/3870112724rsegmnoittet-es.sqlite | Bin .../idb/3870112724rsegmnoittet-es.sqlite-wal | 0 .../profiles/firefox/default-67/times.json | 0 .../profiles/firefox}/tp-67/.parentlock | 0 .../firefox}/tp-67/.startup-incomplete | 0 .../firefox/tp-67/OfflineCache/index.sqlite | Bin .../firefox/tp-67/addonStartup.json.lz4 | Bin .../profiles/firefox/tp-67/addons.json | 0 .../profiles/firefox/tp-67/blocklist.xml | 0 .../tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs | 0 .../cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= | 0 .../0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB | Bin .../1891807939636060566172C08C0AC5783A497AE0 | Bin .../287A739E5A799EFB74FF60A39CE892610F2EFA68 | Bin .../516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 | Bin .../5995C7CCB293DA761DBE3FB34BFE6BB474B55144 | Bin .../5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 | Bin .../60D49F42AAF00DA70E330093AB82F7A3CE4256C3 | Bin .../90942E3BB93C76DCD57A8404751979F24237E461 | Bin .../C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 | Bin .../CD029054D4F0EED4C7FCBE28737398A0390D94A1 | Bin .../CDF359E63200C01C1961DA51E2DC1A04CDBFB351 | Bin .../CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 | Bin .../D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 | Bin .../E21F074DBAD1CB7994F383C419228B689766FB1C | Bin .../profiles/firefox/tp-67/cert9.db | Bin .../profiles/firefox/tp-67/compatibility.ini | 0 .../firefox/tp-67/content-prefs.sqlite | Bin .../profiles/firefox/tp-67/cookies.sqlite | Bin .../profiles/firefox/tp-67/cookies.sqlite-wal | Bin .../firefox/tp-67/datareporting/state.json | 0 .../firefox/tp-67/extension-preferences.json | 0 .../profiles/firefox/tp-67/extensions.json | 0 .../profiles/firefox/tp-67/favicons.sqlite | Bin .../firefox/tp-67/favicons.sqlite-wal | Bin .../profiles/firefox/tp-67/handlers.json | 0 .../profiles/firefox/tp-67/key4.db | Bin .../profiles/firefox/tp-67/permissions.sqlite | Bin .../profiles/firefox/tp-67/pkcs11.txt | 0 .../profiles/firefox/tp-67/places.sqlite | Bin .../profiles/firefox/tp-67/places.sqlite-wal | Bin .../profiles/firefox/tp-67/prefs.js | 0 .../base-track-digest256.pset | Bin .../base-track-digest256.sbstore | Bin .../content-track-digest256.pset | Bin .../content-track-digest256.sbstore | Bin .../mozplugin-block-digest256.pset | Bin .../mozplugin-block-digest256.sbstore | Bin .../mozstd-trackwhite-digest256.sbstore | Bin .../test-block-simple.pset | Bin .../test-block-simple.sbstore | Bin .../test-harmful-simple.pset | Bin .../test-harmful-simple.sbstore | Bin .../test-malware-simple.pset | Bin .../test-malware-simple.sbstore | Bin .../test-phish-simple.pset | Bin .../test-phish-simple.sbstore | Bin .../test-track-simple.pset | Bin .../test-track-simple.sbstore | Bin .../test-trackwhite-simple.pset | Bin .../test-trackwhite-simple.sbstore | Bin .../test-unwanted-simple.pset | Bin .../test-unwanted-simple.sbstore | Bin .../tp-67/safebrowsing/test-block-simple.pset | Bin .../safebrowsing/test-block-simple.sbstore | Bin .../safebrowsing/test-harmful-simple.pset | Bin .../safebrowsing/test-harmful-simple.sbstore | Bin .../safebrowsing/test-malware-simple.pset | Bin .../safebrowsing/test-malware-simple.sbstore | Bin .../tp-67/safebrowsing/test-phish-simple.pset | Bin .../safebrowsing/test-phish-simple.sbstore | Bin .../tp-67/safebrowsing/test-track-simple.pset | Bin .../safebrowsing/test-track-simple.sbstore | Bin .../safebrowsing/test-trackwhite-simple.pset | Bin .../test-trackwhite-simple.sbstore | Bin .../safebrowsing/test-unwanted-simple.pset | Bin .../safebrowsing/test-unwanted-simple.sbstore | Bin .../profiles/firefox/tp-67/search.json.mozlz4 | Bin .../firefox/tp-67/sessionCheckpoints.json | 0 .../profiles/firefox/tp-67/storage.sqlite | Bin .../.metadata | Bin .../.metadata-v2 | Bin .../3647222921wleabcEoxlt-eengsairo.sqlite | Bin ...3647222921wleabcEoxlt-eengsairo.sqlite-wal | 0 .../firefox/tp-67/storage/ls-archive.sqlite | Bin .../tp-67/storage/permanent/chrome/.metadata | Bin .../storage/permanent/chrome/.metadata-v2 | Bin .../1451318868ntouromlalnodry--epcr.sqlite | Bin ...1451318868ntouromlalnodry--epcr.sqlite-wal | 0 .../idb/1657114595AmcateirvtiSty.sqlite | Bin .../idb/1657114595AmcateirvtiSty.sqlite-wal | 0 .../chrome/idb/2918063365piupsah.sqlite | Bin .../chrome/idb/2918063365piupsah.sqlite-wal | 0 .../chrome/idb/3561288849sdhlie.sqlite | Bin .../chrome/idb/3561288849sdhlie.sqlite-wal | 0 .../idb/3870112724rsegmnoittet-es.sqlite | Bin .../idb/3870112724rsegmnoittet-es.sqlite-wal | 0 .../profiles/firefox/tp-67/times.json | 0 .../profiles/firefox2/LittleProxy_MITM.cer | Bin .../profiles/firefox2/tp-67/.parentlock | 0 .../firefox2/tp-67/.startup-incomplete | 0 .../firefox2/tp-67/OfflineCache/index.sqlite | Bin .../firefox2/tp-67/addonStartup.json.lz4 | Bin .../profiles/firefox2/tp-67/addons.json | 0 .../profiles/firefox2/tp-67/blocklist.xml | 0 .../tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs | 0 .../cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= | 0 .../0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB | Bin .../1891807939636060566172C08C0AC5783A497AE0 | Bin .../287A739E5A799EFB74FF60A39CE892610F2EFA68 | Bin .../516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 | Bin .../5995C7CCB293DA761DBE3FB34BFE6BB474B55144 | Bin .../5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 | Bin .../60D49F42AAF00DA70E330093AB82F7A3CE4256C3 | Bin .../90942E3BB93C76DCD57A8404751979F24237E461 | Bin .../C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 | Bin .../CD029054D4F0EED4C7FCBE28737398A0390D94A1 | Bin .../CDF359E63200C01C1961DA51E2DC1A04CDBFB351 | Bin .../CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 | Bin .../D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 | Bin .../E21F074DBAD1CB7994F383C419228B689766FB1C | Bin .../profiles/firefox2/tp-67/cert9.db | Bin .../profiles/firefox2/tp-67/compatibility.ini | 0 .../firefox2/tp-67/content-prefs.sqlite | Bin .../profiles/firefox2/tp-67/cookies.sqlite | Bin .../firefox2/tp-67/cookies.sqlite-wal | Bin .../firefox2/tp-67/datareporting/state.json | 0 .../firefox2/tp-67/extension-preferences.json | 0 .../profiles/firefox2/tp-67/extensions.json | 0 .../profiles/firefox2/tp-67/favicons.sqlite | Bin .../firefox2/tp-67/favicons.sqlite-wal | Bin .../profiles/firefox2/tp-67/handlers.json | 0 .../profiles/firefox2/tp-67/key4.db | Bin .../firefox2/tp-67/permissions.sqlite | Bin .../profiles/firefox2/tp-67/pkcs11.txt | 0 .../profiles/firefox2/tp-67/places.sqlite | Bin .../profiles/firefox2/tp-67/places.sqlite-wal | Bin .../profiles/firefox2/tp-67/prefs.js | 0 .../base-track-digest256.pset | Bin .../base-track-digest256.sbstore | Bin .../content-track-digest256.pset | Bin .../content-track-digest256.sbstore | Bin .../mozplugin-block-digest256.pset | Bin .../mozplugin-block-digest256.sbstore | Bin .../mozstd-trackwhite-digest256.sbstore | Bin .../test-block-simple.pset | Bin .../test-block-simple.sbstore | Bin .../test-harmful-simple.pset | Bin .../test-harmful-simple.sbstore | Bin .../test-malware-simple.pset | Bin .../test-malware-simple.sbstore | Bin .../test-phish-simple.pset | Bin .../test-phish-simple.sbstore | Bin .../test-track-simple.pset | Bin .../test-track-simple.sbstore | Bin .../test-trackwhite-simple.pset | Bin .../test-trackwhite-simple.sbstore | Bin .../test-unwanted-simple.pset | Bin .../test-unwanted-simple.sbstore | Bin .../tp-67/safebrowsing/test-block-simple.pset | Bin .../safebrowsing/test-block-simple.sbstore | Bin .../safebrowsing/test-harmful-simple.pset | Bin .../safebrowsing/test-harmful-simple.sbstore | Bin .../safebrowsing/test-malware-simple.pset | Bin .../safebrowsing/test-malware-simple.sbstore | Bin .../tp-67/safebrowsing/test-phish-simple.pset | Bin .../safebrowsing/test-phish-simple.sbstore | Bin .../tp-67/safebrowsing/test-track-simple.pset | Bin .../safebrowsing/test-track-simple.sbstore | Bin .../safebrowsing/test-trackwhite-simple.pset | Bin .../test-trackwhite-simple.sbstore | Bin .../safebrowsing/test-unwanted-simple.pset | Bin .../safebrowsing/test-unwanted-simple.sbstore | Bin .../firefox2/tp-67/search.json.mozlz4 | Bin .../firefox2/tp-67/sessionCheckpoints.json | 0 .../profiles/firefox2/tp-67/storage.sqlite | Bin .../.metadata | Bin .../.metadata-v2 | Bin .../3647222921wleabcEoxlt-eengsairo.sqlite | Bin ...3647222921wleabcEoxlt-eengsairo.sqlite-wal | 0 .../firefox2/tp-67/storage/ls-archive.sqlite | Bin .../tp-67/storage/permanent/chrome/.metadata | Bin .../storage/permanent/chrome/.metadata-v2 | Bin .../1451318868ntouromlalnodry--epcr.sqlite | Bin ...1451318868ntouromlalnodry--epcr.sqlite-wal | 0 .../idb/1657114595AmcateirvtiSty.sqlite | Bin .../idb/1657114595AmcateirvtiSty.sqlite-wal | 0 .../chrome/idb/2918063365piupsah.sqlite | Bin .../chrome/idb/2918063365piupsah.sqlite-wal | 0 .../chrome/idb/3561288849sdhlie.sqlite | Bin .../chrome/idb/3561288849sdhlie.sqlite-wal | 0 .../idb/3870112724rsegmnoittet-es.sqlite | Bin .../idb/3870112724rsegmnoittet-es.sqlite-wal | 0 .../profiles/firefox2/tp-67/times.json | 0 test/availability/test_folders.py | 6 +- test/http_collector/test_collector.py | 16 +- .../test_biggest_gap_bisection_search.py | 4 +- .../test_biggest_gap_bisection_sequence.py | 4 +- test/sequence/test_composite_search.py | 4 +- test/sequence/test_sequence_strategy.py | 8 +- test/states/test_state_result.py | 83 ++-- 1205 files changed, 7961 insertions(+), 3210 deletions(-) delete mode 100644 bci/analysis/plot_factory.py delete mode 100644 bci/browser/automation/terminal.py delete mode 100644 bci/browser/binary/vendors/chromium.py delete mode 100644 bci/browser/binary/vendors/firefox.py delete mode 100644 bci/browser/statistics.py delete mode 100644 bci/browser/support.py delete mode 100644 bci/database/mongo/revision_cache.py delete mode 100644 bci/evaluations/collectors/collector.py delete mode 100644 bci/evaluations/custom/custom_evaluation.py delete mode 100644 bci/evaluations/evaluation_framework.py delete mode 100644 bci/evaluations/experiments.py delete mode 100644 bci/evaluations/logic.py delete mode 100644 bci/version_control/repository/online/chromium.py delete mode 100644 bci/version_control/repository/online/firefox.py delete mode 100644 bci/version_control/state_result_factory.py delete mode 100644 bci/version_control/states/revisions/base.py delete mode 100644 bci/version_control/states/revisions/chromium.py delete mode 100644 bci/version_control/states/revisions/firefox.py delete mode 100644 bci/version_control/states/state.py delete mode 100644 bci/version_control/states/versions/chromium.py delete mode 100644 bci/version_control/states/versions/firefox.py delete mode 100644 bci/web/blueprints/api.py delete mode 100644 bci/worker.py rename {bci => bughog}/__init__.py (100%) rename {bci => bughog}/analysis/__init__.py (100%) create mode 100644 bughog/analysis/plot_factory.py rename {bci => bughog}/app.py (84%) rename {bci => bughog}/cli.py (100%) rename {bci => bughog}/configuration.py (95%) rename {bci/browser => bughog/database}/__init__.py (100%) rename {bci/browser/automation => bughog/database/mongo}/__init__.py (100%) create mode 100644 bughog/database/mongo/cache.py rename {bci => bughog}/database/mongo/container.py (97%) rename bci/database/mongo/binary_cache.py => bughog/database/mongo/executable_cache.py (50%) rename {bci => bughog}/database/mongo/mongodb.py (51%) rename {bci => bughog}/distribution/worker_manager.py (75%) rename {bci/browser/binary => bughog/evaluation}/__init__.py (100%) rename {bci/evaluations => bughog/evaluation}/collectors/base.py (59%) create mode 100644 bughog/evaluation/collectors/collector.py rename {bci/evaluations => bughog/evaluation}/collectors/logs.py (74%) rename {bci/evaluations => bughog/evaluation}/collectors/requests.py (90%) create mode 100644 bughog/evaluation/custom/custom_evaluation.py create mode 100644 bughog/evaluation/evaluation.py create mode 100644 bughog/evaluation/experiment.py create mode 100644 bughog/evaluation/experiment_result.py create mode 100644 bughog/evaluation/file_structure.py rename {bci/browser/configuration => bughog/integration_tests}/__init__.py (100%) rename {bci => bughog}/integration_tests/evaluation_configurations.py (67%) rename {bci => bughog}/integration_tests/verify_results.py (64%) rename {bci => bughog}/main.py (70%) create mode 100644 bughog/parameters.py rename {bci/browser/interaction => bughog/search_strategy}/__init__.py (100%) rename {bci => bughog}/search_strategy/bgb_search.py (95%) rename {bci => bughog}/search_strategy/bgb_sequence.py (95%) rename {bci => bughog}/search_strategy/composite_search.py (75%) rename {bci => bughog}/search_strategy/sequence_strategy.py (89%) rename {bci/database => bughog/subject}/__init__.py (100%) create mode 100644 bughog/subject/base.py create mode 100644 bughog/subject/evaluation_framework.py create mode 100644 bughog/subject/executable.py create mode 100644 bughog/subject/factory.py create mode 100644 bughog/subject/interaction.py create mode 100644 bughog/subject/state_oracle.py rename {bci/database/mongo => bughog/subject/webbrowser}/__init__.py (100%) create mode 100644 bughog/subject/webbrowser/base.py rename {bci/evaluations => bughog/subject/webbrowser/binary}/__init__.py (100%) rename {bci/browser => bughog/subject/webbrowser}/binary/artisanal_manager.py (57%) rename {bci/browser => bughog/subject/webbrowser}/binary/binary.py (81%) rename {bci/browser => bughog/subject/webbrowser}/binary/factory.py (61%) create mode 100644 bughog/subject/webbrowser/binary/vendors/chromium.py create mode 100644 bughog/subject/webbrowser/binary/vendors/firefox.py rename {bci/evaluations/custom => bughog/subject/webbrowser/chromium}/__init__.py (100%) rename bci/browser/configuration/chromium.py => bughog/subject/webbrowser/chromium/executable.py (59%) create mode 100644 bughog/subject/webbrowser/chromium/repo.py create mode 100644 bughog/subject/webbrowser/chromium/state_oracle.py create mode 100644 bughog/subject/webbrowser/chromium/subject.py rename {bci/browser => bughog/subject/webbrowser}/cli_options/chromium.py (100%) rename {bci/browser => bughog/subject/webbrowser}/cli_options/firefox.py (100%) rename {bci/integration_tests => bughog/subject/webbrowser/configuration}/__init__.py (100%) rename {bci/browser => bughog/subject/webbrowser}/configuration/browser.py (61%) create mode 100644 bughog/subject/webbrowser/configuration/chromium.py rename {bci/browser => bughog/subject/webbrowser}/configuration/firefox.py (94%) rename {bci/browser => bughog/subject/webbrowser}/configuration/options.py (100%) create mode 100644 bughog/subject/webbrowser/evaluation.py create mode 100644 bughog/subject/webbrowser/executable.py rename {bci/search_strategy => bughog/subject/webbrowser/firefox}/__init__.py (100%) create mode 100644 bughog/subject/webbrowser/firefox/executable.py create mode 100644 bughog/subject/webbrowser/firefox/repo.py create mode 100644 bughog/subject/webbrowser/firefox/state_oracle.py create mode 100644 bughog/subject/webbrowser/firefox/subject.py rename {bci/version_control => bughog/subject/webbrowser/interaction}/__init__.py (100%) rename {bci/browser => bughog/subject/webbrowser}/interaction/elements/five.png (100%) rename {bci/browser => bughog/subject/webbrowser}/interaction/elements/four.png (100%) rename {bci/browser => bughog/subject/webbrowser}/interaction/elements/one.png (100%) rename {bci/browser => bughog/subject/webbrowser}/interaction/elements/six.png (100%) rename {bci/browser => bughog/subject/webbrowser}/interaction/elements/three.png (100%) rename {bci/browser => bughog/subject/webbrowser}/interaction/elements/two.png (100%) rename {bci/browser => bughog/subject/webbrowser}/interaction/interaction.py (64%) rename {bci/browser => bughog/subject/webbrowser}/interaction/simulation.py (77%) rename {bci/browser => bughog/subject/webbrowser}/interaction/simulation_exception.py (100%) rename {bci/browser/configuration => bughog/subject/webbrowser}/profile.py (96%) rename {bci/version_control/repository => bughog/subject/webbrowser}/repository.py (100%) create mode 100644 bughog/subject/webbrowser/state_cache.py rename {bci => bughog}/util.py (100%) rename {bci/version_control/repository => bughog/version_control}/__init__.py (100%) rename {bci => bughog}/version_control/revision_parser/chromium_parser.py (91%) rename {bci => bughog}/version_control/revision_parser/parser.py (100%) create mode 100644 bughog/version_control/state/base.py create mode 100644 bughog/version_control/state/commit/base.py create mode 100644 bughog/version_control/state/commit/chromium.py create mode 100644 bughog/version_control/state/commit/firefox.py rename {bci/version_control/states/versions => bughog/version_control/state/release}/base.py (69%) create mode 100644 bughog/version_control/state/release/chromium.py create mode 100644 bughog/version_control/state/release/firefox.py rename {bci => bughog}/version_control/state_factory.py (57%) rename {bci/version_control/repository/online => bughog/web}/__init__.py (100%) create mode 100644 bughog/web/blueprints/api.py rename {bci => bughog}/web/blueprints/experiments.py (98%) rename {bci => bughog}/web/blueprints/test.py (71%) rename {bci => bughog}/web/clients.py (82%) rename {bci => bughog}/web/templates/base.html (100%) rename {bci => bughog}/web/templates/cookies.html (100%) rename {bci => bughog}/web/templates/experiment.html (100%) rename {bci => bughog}/web/templates/integration_tests.html (100%) rename {bci => bughog}/web/vue/.gitignore (100%) rename {bci => bughog}/web/vue/index.html (100%) rename {bci => bughog}/web/vue/package-lock.json (100%) rename {bci => bughog}/web/vue/package.json (100%) rename {bci => bughog}/web/vue/postcss.config.js (100%) rename {bci => bughog}/web/vue/public/.gitkeep (100%) rename {bci => bughog}/web/vue/src/App.vue (90%) rename {bci => bughog}/web/vue/src/components/evaluation_status.vue (100%) rename {bci => bughog}/web/vue/src/components/gantt.vue (100%) rename {bci => bughog}/web/vue/src/components/poc-editor.vue (100%) rename {bci => bughog}/web/vue/src/components/section-header.vue (78%) rename {bci => bughog}/web/vue/src/components/tooltip.vue (100%) rename {bci => bughog}/web/vue/src/interaction_script_mode.js (100%) rename {bci => bughog}/web/vue/src/main.js (100%) rename {bci => bughog}/web/vue/src/style.css (100%) rename {bci => bughog}/web/vue/tailwind.config.js (100%) rename {bci => bughog}/web/vue/vite.config.js (100%) create mode 100644 bughog/worker.py delete mode 100644 experiments/README.md delete mode 100644 experiments/pages/CSP/c320796/a.test/main/index.html create mode 100755 subject/webbrowser/executable/chromium/artisanal/meta.json create mode 100755 subject/webbrowser/executable/firefox/artisanal/Ladybird create mode 100755 subject/webbrowser/executable/firefox/artisanal/meta.json rename {bci/evaluations/custom/default_files => subject/webbrowser/experiments/_default_files}/headers.json (100%) rename {bci/evaluations/custom/default_files => subject/webbrowser/experiments/_default_files}/html (100%) rename {bci/evaluations/custom/default_files => subject/webbrowser/experiments/_default_files}/js (100%) rename {bci/evaluations/custom/default_files => subject/webbrowser/experiments/_default_files}/py (100%) rename {bci/evaluations/custom/default_files => subject/webbrowser/experiments/_default_files}/script.cmd (100%) rename {bci/evaluations/custom/default_files => subject/webbrowser/experiments/_default_files}/url_queue.txt (100%) rename bci/web/__init__.py => subject/webbrowser/experiments/webbrowser/Dockerfile (100%) rename {experiments/pages/Support/PythonServer/a.test/main => subject/webbrowser/experiments/webbrowser/pages/BlackHat/all-zero/adition.com/main2}/headers.json (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/all-zero/adition.com/main2/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/all-zero/leak.test/helper/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/all-zero/leak.test/helper/index.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/all-zero/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/all-zero/leak.test/main/index.html rename {experiments/pages/IntegrationTests/all_reproduced => subject/webbrowser/experiments/webbrowser/pages/BlackHat/cross-origin-frame-redirect}/a.test/main/headers.json (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/cross-origin-frame-redirect/a.test/main/index.html rename {experiments/pages/IntegrationTests/click/a.test => subject/webbrowser/experiments/webbrowser/pages/BlackHat/cross-origin-frame-redirect/leak.test}/main/headers.json (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/cross-origin-frame-redirect/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/cross-origin-frame-redirect/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/form-blank/a.test/main.py create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/form-blank/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/form-blank/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/form-blank/script.cmd create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/form-target-blank-fast-forward/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/form-target-blank-fast-forward/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/form-target-blank-fast-forward/script.cmd create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/insecure-loads-local-embed/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/insecure-loads-local-embed/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/insecure-loads-local-embed/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/nonce-stealing/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/nonce-stealing/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/picture/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/picture/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/spoof-referer/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/spoof-referer/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/test/script.cmd rename {experiments/pages/IntegrationTests/none_reproduced/a.test => subject/webbrowser/experiments/webbrowser/pages/BlackHat/upgrade-insecure-requests-link/leak.test}/main/headers.json (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/upgrade-insecure-requests-link/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/BlackHat/upgrade-insecure-requests-link/script.cmd rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1001283/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1001283/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1001283/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1001283/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1001982-img/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1001982-img/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1001982-img/leak.test/helper/index.html (100%) rename {experiments/pages/Support/Downloading => subject/webbrowser/experiments/webbrowser/pages/CSP/c1001982-meta}/a.test/main/headers.json (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1001982-meta/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1001982-meta/leak.test/helper/headers.json rename {experiments/pages/CSP/c1001982 => subject/webbrowser/experiments/webbrowser/pages/CSP/c1001982-meta}/leak.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1001982/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1001982/a.test/main/index.html (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1001982/leak.test/helper/index.html rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1064676/leak.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1064676/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1064676/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1064676/url_queue.txt (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1072719-meta/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1072719-meta/leak.test/main/index.html rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1072719/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1072719/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1074317/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1074317/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1074317/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1074317/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1074317/a.test/script/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1107824/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1107824/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1107824/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1107824/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1107824/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1109167/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1109167/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1109167/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115045-img/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115045-img/leak.test/main/index.html (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1115045-meta/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1115045-meta/leak.test/main/index.html rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115045/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115045/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115045/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115298-img/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115298-img/leak.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115298-img/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115298/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115298/leak.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115298/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115298/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628-WebKitBlob/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628-WebKitBlob/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628-WebKitBlob/a.test/secret/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628-WebKitBlob/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628-WebKitBlob/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628-img/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628-img/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628-img/a.test/secret/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628-img/leak.test/main/index.html (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1115628-meta/a.test/helper/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1115628-meta/a.test/helper/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1115628-meta/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c1115628-meta/leak.test/main/index.html rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628/a.test/secret/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1115628/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1117687/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1117687/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1117687/a.test/secret/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1117687/a.test/secret/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1117687/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1117687/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1180759/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1180759/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1233067/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1233067/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1248289/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1248289/a.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1248289/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1248289/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1248289/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1259077/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1259077/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1259077/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1259077/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1291482/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1291482/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1329460/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1329460/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1339146/leak.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1339146/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c1339146/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c320796/a.test/main/headers.json (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c320796/a.test/main/index.html rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c358471/leak.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c358471/leak.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c358471/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c358471/leak.test/worker/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c358471/leak.test/worker/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c377995/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c377995/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c377995/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c377995/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c393401/leak.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c393401/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c393401/leak.test/script/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c411600/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c411600/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c411600/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c482558-img-src/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c482558-img-src/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c482558-img-src/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c482558/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c482558/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c482558/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c487155/a.test/helper1/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c487155/a.test/helper2/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c487155/a.test/helper2/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c487155/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c534542-iframe/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c534542-iframe/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c534542-iframe/sub.a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c534542/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c534542/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c534542/sub.a.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c560695-connect-src/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c560695/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c560695/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c582387/a.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c582387/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c582387/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c582387/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c590505-iframe/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c590505-iframe/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c590505-img/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c590505-img/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c590505/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c590505/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c605451/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c605451/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c610441/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c610441/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c630332/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c630332/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c633348/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c633348/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c633348/leak.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c661126/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c661126/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c661852-form-action/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c661852-form-action/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c661852-form-action/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c661852-form-action/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c661852/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c661852/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c661852/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c661852/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c663620-img/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c663620-img/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c663620/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c663620/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c669086-img/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c669086-img/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c669086-script/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c669086-script/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c669086/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c669086/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c682673-script/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c682673-script/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c682673/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c682673/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c689412-iframe/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c689412-iframe/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c689412/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c689412/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c696806/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c696806/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c732779/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c732779/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c732779/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c732779/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c740615/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c740615/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c747847-iframe/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c747847-iframe/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c747847-img/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c747847-img/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c767635/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c767635/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c767635/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c777350/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c777350/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c777350/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747-iframe/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747-iframe/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747-img/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747-img/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747-only-script-src/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747-only-script-src/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747-only-script-src/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747-x-webkit-csp/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747-x-webkit-csp/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c799747/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c811691-only-object-src/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c811691-only-object-src/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c811691/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c811691/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c811691/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c845961/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c845961/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c894228-x-webkit-csp/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c894228-x-webkit-csp/leak.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c894228-x-webkit-csp/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c894228/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c894228/leak.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c894228/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c894228/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c894228/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c908207/a.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c908207/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c908207/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c909865/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c909865/a.test/helper2/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c909865/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c909865/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c916326/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c916326/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c916326/a.test/helper2/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c916326/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c932892/leak.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c932892/leak.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c932892/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c932892/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c941340/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c941340/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c941340/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c955350-img/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c955350-img/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c955350-script/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c955350-script/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c955350/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c955350/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c955350/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c957606-object/a.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c957606-object/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c957606-object/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c957606/a.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c957606/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c957606/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c957606/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c967780/a.test/download/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c967780/a.test/download/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c967780/a.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c967780/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c967780/a.test/main/index.html (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c971231-meta/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/c971231-meta/leak.test/main/index.html rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c971231-object/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c971231-object/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c971231/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c971231/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c971231/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c990581-x-webkit-csp/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c990581-x-webkit-csp/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c990581/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c990581/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c990581/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c992698/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c992698/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/c992698/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1007634/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1007634/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1007634/a.test/worker/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1007634/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1036399/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1036399/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1036399/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-frame/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-frame/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-frame/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-frame/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-img/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-img/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-img/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-img/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-script/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-script/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-script/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-script/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-without-sandbox/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-without-sandbox/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-without-sandbox/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952-without-sandbox/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1073952/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1086999-img/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1086999-img/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1086999-script/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1086999-script/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1086999/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1086999/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1086999/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1208559-script-src/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1208559-script-src/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1208559/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1208559/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1208559/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1223743/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1223743/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1223743/url_queue.txt (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/f1296976-meta/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/f1296976-meta/a.test/main/index.html rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1296976/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1296976/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1296976/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1312272/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1312272/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1312272/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1316826/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1316826/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1316826/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1377426-frame/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1377426-frame/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1377426-img/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1377426-img/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1377426/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1377426/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1377426/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1396320/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1396320/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1396320/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1416045/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1416045/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1416045/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1422924/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1422924/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1422924/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1432358/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1432358/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1441468/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1441468/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1441468/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1457100/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1457100/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1457100/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1460538-style-src/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1460538-style-src/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1460538/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1460538/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1460538/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1469150/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1469150/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1469150/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1469150/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1469150/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1505412/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1505412/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1505412/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1505412/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1505412/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1542194/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1542194/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1542194/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1542194/leak.test/helper2/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1548385/a.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1548385/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1548385/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1550414/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1550414/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1550414/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1550414/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1605814/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1605814/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1605814/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1644076/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1644076/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1644076/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1644076/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1644790/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1644790/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1738418/leak.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1738418/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f1738418/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f774136/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f774136/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f784158/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f784158/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f886164/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f886164/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f886164/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f908824/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f908824/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f908933/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f908933/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f908933/url_queue.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f910139/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f910139/a.test/main/index.xml (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f949706/leak.test/helper/index.css (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f949706/leak.test/helper2/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f949706/leak.test/helper2/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f949706/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f949706/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/CSP/f949706/url_queue.txt (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/CSP/test8/sub.a.test/main/headers.json rename browser/extensions/chromium/.gitkeep => subject/webbrowser/experiments/webbrowser/pages/CSP/test8/sub.a.test/main/index.html (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/audio/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/audio/leak.test/script/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/audio/leak.test/script/index.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/audio/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c50-64/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c50-64/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c65-76/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c65-76/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c65-76/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c77-85/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c77-85/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c77-85/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c86-90/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c86-90/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c86-90/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c91-/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/c91-/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/debug-c77/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/debug-c77/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/debug-c77/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/f2/leak.test/main/headers.json rename {experiments/pages/IntegrationTests/none_reproduced/a.test => subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/f2/leak.test}/main/index.html (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/f3/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/f3/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/fingerprint1/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/fingerprint1/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/simple/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Fingerprinting/simple/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c1001283/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c1001283/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c1001283/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c1001982/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c1001982/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c320796/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c320796/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c358471/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c358471/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c358471/a.test/worker/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c377995/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c377995/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c377995/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c482558/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c482558/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c534542/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c534542/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c534542/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c560695/a.test/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c560695/a.test/manifest.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c560695/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c560695/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c560695/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c582387/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c582387/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c582387/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c590505/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c590505/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c590505/a.test/script/index.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c605451/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c605451/leak.test/main/index.html rename {experiments/pages/Support/upgrade-insecure-requests => subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c610441}/a.test/main/headers.json (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c610441/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c630332/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c630332/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c630332/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c633348/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c633348/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c633348/b.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c661126/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c661126/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c661126/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c663620/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c663620/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c663620/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c669086/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c669086/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c669086/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c682673/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c682673/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c682673/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c689412/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c689412/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c689412/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c696806/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c696806/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c696806/leak.test/manifest.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c696806/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c732779/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c732779/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c732779/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c740615/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c740615/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c740615/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c767635/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c767635/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c767635/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c777350/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c777350/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c777350/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c799747/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c799747/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c799747/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c811691/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c811691/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c845961/b.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c845961/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c894228/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c894228/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c908207/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c908207/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c908207/leak.test/xss.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c908207/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c909865/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c909865/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c909865/a.test/test_frame/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c909865/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c916326/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c916326/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c916326/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c932892/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c932892/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c941340/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c941340/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c941340/leak.test/module/index.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c955350/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c955350/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c955350/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c957606/a.test/index/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c957606/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c967780/a.test/download/index.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c967780/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c967780/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c967780/server.py create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c967780/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c971231/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c971231/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c990581/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c990581/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/c992698/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1007634/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1007634/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1007634/a.test/worker/index.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1036399/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1036399/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1036399/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1312272/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1312272/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1312272/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1316826/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1316826/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1316826/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1377426/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1377426/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1377426/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1396320/leak.test/helper/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1396320/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1396320/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1396320/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1422924/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1422924/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1432358/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1432358/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1441468/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1441468/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1457100/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1457100/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1469150/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1469150/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1505412/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1505412/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1542194/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1548385/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1548385/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1548385/a.test/page/index.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1550414/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1550414/a.test/xs/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1550414/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1605814/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1605814/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1605814/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1644076/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1644076/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1644076/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1644790/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1644790/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1644790/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1738418/a.test/child/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1738418/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1738418/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f1738418/url_queue.txt create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f886164/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f886164/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f949706/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/GenAI-38-Examples-4o-mini/f949706/leak.test/main/worker.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/cookie-prefix-bypass/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/cookie-prefix-bypass/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/csp-overwrite/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/csp-overwrite/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/nonce-stealing/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/nonce-stealing/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/nonce-stealing/script.cmd create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/prerender-bypass/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/prerender-bypass/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/referer-spoofing/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IndustryDay/referer-spoofing/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IntegrationTests/all_reproduced/a.test/main/headers.json rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/IntegrationTests/all_reproduced/a.test/main/index.html (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IntegrationTests/click/a.test/main/headers.json rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/IntegrationTests/click/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/IntegrationTests/click/script.cmd (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IntegrationTests/none_reproduced/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/IntegrationTests/none_reproduced/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c323695987/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c323695987/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c323695987/b.test/victim/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c323695987/b.test/victim/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c323695987/script.cmd create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40051153/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40051153/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40051153/script.cmd create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40051484/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40051484/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40051484/b.test/server.py create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40051484/b.test/set-cookie/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40051484/b.test/set-cookie/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40051484/script.cmd create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40052447/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40052447/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40052447/script.cmd create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40095912-transpiled/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40095912-transpiled/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40095912-transpiled/a.test/serviceworker/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40095912-transpiled/a.test/serviceworker/index.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40095912-transpiled/script.cmd create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40095912/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40095912/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40095912/a.test/serviceworker/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40095912/a.test/serviceworker/index.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/SOP/c40095912/script.cmd rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/AutoGUI/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/AutoGUI/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/AutoGUI/script.cmd (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/BTPC/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/BTPC/url_queue.txt (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/CSP-in-meta-unsafe-inline/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/CSP-in-meta-unsafe-inline/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/CSP-in-meta/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/CSP-in-meta/leak.test/main/index.html rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/CSP/a.test/helper/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/CSP/a.test/helper/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/CSP/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/CSP/url_queue.txt (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/Downloading/a.test/main/headers.json rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/Downloading/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/Downloading/script.cmd (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/PythonServer/a.test/main/headers.json rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/PythonServer/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/PythonServer/a.test/server.py (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/ServiceWorker/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/ServiceWorker/url_queue.txt (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/StatusCode/leak.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/StatusCode/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/c1074317/a.test/helper/headers.json rename browser/extensions/firefox/.gitkeep => subject/webbrowser/experiments/webbrowser/pages/Support/c1074317/a.test/helper/index.html (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/c1074317/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/c1074317/a.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/c1074317/a.test/script/index.js create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/c740615/leak.test/main/index.html create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/down/url_queue.txt rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/form-action/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/form-action/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/not-allow-modals/a.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/not-allow-modals/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/not-script-src-elem/leak.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/not-script-src-elem/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/not-script-src-elem/leak.test/main/index.html (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/portal/a.test/main/headers.json create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/portal/a.test/main/index.html rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/securitypolicyviolation/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/securitypolicyviolation/leak.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/strict-dynamic/leak.test/helper/index.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/strict-dynamic/leak.test/main/headers.json (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/strict-dynamic/leak.test/main/index.html (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/pages/Support/upgrade-insecure-requests/a.test/main/headers.json rename {experiments => subject/webbrowser/experiments/webbrowser}/pages/Support/upgrade-insecure-requests/a.test/main/index.html (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/600x400.png (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/CSP/c756018.as (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/CSP/c756018.swf (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/big-gradient.jpg (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/black-transparent.png (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/btc.pdf (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/bughog.css (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/bughog.ico (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/bughog.js (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/bw.png (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/font.woff (100%) create mode 100644 subject/webbrowser/experiments/webbrowser/res/horse.mp3 rename {experiments => subject/webbrowser/experiments/webbrowser}/res/horse.ogg (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/rgb.png (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/short-text.txt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/subtitles.vtt (100%) rename {experiments => subject/webbrowser/experiments/webbrowser}/res/video.webm (100%) rename {browser => subject/webbrowser}/extensions/README.md (100%) rename browser/profiles/chromium/59_btpc/First Run => subject/webbrowser/extensions/chromium/.gitkeep (100%) rename browser/profiles/firefox/default-67/.parentlock => subject/webbrowser/extensions/firefox/.gitkeep (100%) rename {browser => subject/webbrowser}/profiles/chromium/17_btpc/Default/Preferences (100%) rename {browser => subject/webbrowser}/profiles/chromium/24_btpc/Default/Preferences (100%) rename {browser => subject/webbrowser}/profiles/chromium/36_btpc/Default/Preferences (100%) rename {browser => subject/webbrowser}/profiles/chromium/40_btpc/Default/Preferences (100%) rename {browser => subject/webbrowser}/profiles/chromium/46_btpc/Default/Preferences (100%) rename {browser => subject/webbrowser}/profiles/chromium/59_btpc/BrowserMetrics-active.pma (100%) rename {browser => subject/webbrowser}/profiles/chromium/59_btpc/Default/Preferences (100%) rename browser/profiles/firefox/default-67/.startup-incomplete => subject/webbrowser/profiles/chromium/59_btpc/First Run (100%) rename {browser => subject/webbrowser}/profiles/chromium/59_btpc/Local State (100%) rename {browser => subject/webbrowser}/profiles/chromium/6_btpc/Default/Preferences (100%) rename {browser => subject/webbrowser}/profiles/firefox/cert8.db (100%) rename {browser/profiles/firefox/tp-67 => subject/webbrowser/profiles/firefox/default-67}/.parentlock (100%) rename {browser/profiles/firefox/tp-67 => subject/webbrowser/profiles/firefox/default-67}/.startup-incomplete (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/OfflineCache/index.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/addonStartup.json.lz4 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/addons.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/blocklist.xml (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cert9.db (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/compatibility.ini (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/content-prefs.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cookies.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/cookies.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/datareporting/state.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/extension-preferences.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/extensions.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/favicons.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/favicons.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/handlers.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/key4.db (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/permissions.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/pkcs11.txt (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/places.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/places.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/prefs.js (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/base-track-digest256.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/base-track-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/content-track-digest256.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/content-track-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/mozplugin-block-digest256.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/mozplugin-block-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-block-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-block-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-harmful-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-harmful-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-malware-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-malware-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-phish-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-phish-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-track-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-track-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-trackwhite-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-trackwhite-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-unwanted-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing-updating/test-unwanted-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-block-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-block-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-harmful-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-harmful-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-malware-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-malware-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-phish-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-phish-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-track-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-track-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-trackwhite-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-trackwhite-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-unwanted-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/safebrowsing/test-unwanted-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/search.json.mozlz4 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/sessionCheckpoints.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/ls-archive.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/.metadata (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/.metadata-v2 (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/default-67/times.json (100%) rename {browser/profiles/firefox2 => subject/webbrowser/profiles/firefox}/tp-67/.parentlock (100%) rename {browser/profiles/firefox2 => subject/webbrowser/profiles/firefox}/tp-67/.startup-incomplete (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/OfflineCache/index.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/addonStartup.json.lz4 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/addons.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/blocklist.xml (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cert9.db (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/compatibility.ini (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/content-prefs.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cookies.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/cookies.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/datareporting/state.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/extension-preferences.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/extensions.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/favicons.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/favicons.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/handlers.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/key4.db (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/permissions.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/pkcs11.txt (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/places.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/places.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/prefs.js (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/base-track-digest256.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/base-track-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/content-track-digest256.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/content-track-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/mozplugin-block-digest256.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/mozplugin-block-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-block-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-block-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-harmful-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-harmful-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-malware-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-malware-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-phish-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-phish-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-track-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-track-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-trackwhite-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-trackwhite-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-unwanted-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing-updating/test-unwanted-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-block-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-block-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-harmful-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-harmful-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-malware-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-malware-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-phish-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-phish-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-track-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-track-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-trackwhite-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-trackwhite-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-unwanted-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/safebrowsing/test-unwanted-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/search.json.mozlz4 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/sessionCheckpoints.json (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/ls-archive.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/.metadata (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/.metadata-v2 (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox/tp-67/times.json (100%) rename {browser => subject/webbrowser}/profiles/firefox2/LittleProxy_MITM.cer (100%) rename browser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs => subject/webbrowser/profiles/firefox2/tp-67/.parentlock (100%) rename browser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= => subject/webbrowser/profiles/firefox2/tp-67/.startup-incomplete (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/OfflineCache/index.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/addonStartup.json.lz4 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/addons.json (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/blocklist.xml (100%) rename browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal => subject/webbrowser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs (100%) rename browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal => subject/webbrowser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cert9.db (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/compatibility.ini (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/content-prefs.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cookies.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/cookies.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/datareporting/state.json (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/extension-preferences.json (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/extensions.json (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/favicons.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/favicons.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/handlers.json (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/key4.db (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/permissions.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/pkcs11.txt (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/places.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/places.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/prefs.js (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/base-track-digest256.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/base-track-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/content-track-digest256.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/content-track-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/mozplugin-block-digest256.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/mozplugin-block-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-block-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-block-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-harmful-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-harmful-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-malware-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-malware-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-phish-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-phish-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-track-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-track-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-trackwhite-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-trackwhite-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-unwanted-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing-updating/test-unwanted-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-block-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-block-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-harmful-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-harmful-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-malware-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-malware-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-phish-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-phish-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-track-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-track-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-trackwhite-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-trackwhite-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-unwanted-simple.pset (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/safebrowsing/test-unwanted-simple.sbstore (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/search.json.mozlz4 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/sessionCheckpoints.json (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite (100%) rename browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal => subject/webbrowser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/ls-archive.sqlite (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/permanent/chrome/.metadata (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/permanent/chrome/.metadata-v2 (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite (100%) rename browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal => subject/webbrowser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite (100%) rename browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal => subject/webbrowser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite (100%) rename browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal => subject/webbrowser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal (100%) rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite (100%) create mode 100644 subject/webbrowser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite (100%) create mode 100644 subject/webbrowser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal rename {browser => subject/webbrowser}/profiles/firefox2/tp-67/times.json (100%) diff --git a/Dockerfile b/Dockerfile index 114d26a6..e7d19f42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,12 +55,12 @@ RUN curl -sSLo multiarch-support.deb http://security.debian.org/debian-security/ ln -s /usr/lib/x86_64-linux-gnu/libnspr4.so /usr/lib/x86_64-linux-gnu/libnspr4.so.0d RUN mkdir -p /app/logs && \ - mkdir -p /app/subject/browser/binaries/chromium/downloaded && \ - mkdir -p /app/subject/browser/binaries/firefox/downloaded && \ - mkdir -p /app/subject/browser/binaries/chromium/artisanal && \ - mkdir -p /app/subject/browser/binaries/firefox/artisanal + mkdir -p /app/subject/webbrowser/binaries/chromium/downloaded && \ + mkdir -p /app/subject/webbrowser/binaries/firefox/downloaded && \ + mkdir -p /app/subject/webbrowser/binaries/chromium/artisanal && \ + mkdir -p /app/subject/webbrowser/binaries/firefox/artisanal -COPY subject/browser/profiles /app/subject/browser/profiles +COPY subject/webbrowser/profiles /app/subject/webbrowser/profiles COPY --chmod=0755 scripts/ /app/scripts/ RUN cp /app/scripts/daemon/xvfb /etc/init.d/xvfb @@ -72,7 +72,7 @@ RUN uv sync RUN touch /root/.Xauthority && \ xauth add ${HOST}:0 . $(xxd -l 16 -p /dev/urandom) - + FROM base AS core # Copy rest of source code COPY bughog /app/bughog diff --git a/bci/analysis/plot_factory.py b/bci/analysis/plot_factory.py deleted file mode 100644 index a9682d20..00000000 --- a/bci/analysis/plot_factory.py +++ /dev/null @@ -1,73 +0,0 @@ -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import PlotParameters -from bci.version_control.state_result_factory import StateResultFactory - - -class PlotFactory: - - @staticmethod - def get_plot_revision_data(params: PlotParameters) -> dict: - revision_docs = MongoDB().get_documents_for_plotting(params) - revision_results = PlotFactory.__add_outcome_info(params, revision_docs) - return revision_results - - @staticmethod - def get_plot_version_data(params: PlotParameters) -> dict: - version_docs = MongoDB().get_documents_for_plotting(params, releases=True) - version_results = PlotFactory.__add_outcome_info(params, version_docs) - return version_results - - @staticmethod - def validate_params(params: PlotParameters) -> list[str]: - missing_parameters = [] - if not params.mech_group: - missing_parameters.append('selected experiment') - if not params.target_mech_id: - missing_parameters.append('reproduction ID') - if not params.browser_name: - missing_parameters.append('browser') - if not params.database_collection: - missing_parameters.append('database collection') - return missing_parameters - - @staticmethod - def __transform_to_bokeh_compatible(docs: list) -> dict: - new_docs = {} - for d in docs: - for key, value in d.items(): - if key not in new_docs: - new_docs[key] = [] - new_docs[key].append(value) - return new_docs - - @staticmethod - def __add_outcome_info(params: PlotParameters, docs: list): - if not docs: - return { - 'revision_number': [], - 'browser_version': [], - 'browser_version_str': [], - 'outcome': [] - } - docs_with_outcome = [] - state_result_factory = StateResultFactory(params.mech_group) - - for doc in docs: - state_result_data = doc['results'] - state_result = state_result_factory.get_result(state_result_data) - new_doc = { - 'revision_number': doc['state']['revision_number'], - 'browser_version': int(doc['browser_version'].split('.')[0]), - 'browser_version_str': doc['browser_version'].split('.')[0] - } - if state_result.is_dirty: - new_doc['outcome'] = 'Error' - docs_with_outcome.append(new_doc) - elif state_result.reproduced: - new_doc['outcome'] = 'Reproduced' - docs_with_outcome.append(new_doc) - else: - new_doc['outcome'] = 'Not reproduced' - docs_with_outcome.append(new_doc) - docs_with_outcome = PlotFactory.__transform_to_bokeh_compatible(docs_with_outcome) - return docs_with_outcome diff --git a/bci/browser/automation/terminal.py b/bci/browser/automation/terminal.py deleted file mode 100644 index ddc08b8e..00000000 --- a/bci/browser/automation/terminal.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -import signal -import subprocess -import time - -logger = logging.getLogger(__name__) - - -class TerminalAutomation: - @staticmethod - def visit_url(url: str, args: list[str], seconds_per_visit: int): - args.append(url) - proc = TerminalAutomation.open_browser(args) - logger.debug(f'Visiting the page for {seconds_per_visit}s') - time.sleep(seconds_per_visit) - TerminalAutomation.terminate_browser(proc, args) - - @staticmethod - def open_browser(args: list[str]) -> subprocess.Popen: - logger.debug('Starting browser process...') - logger.debug(f'Command string: \'{" ".join(args)}\'') - with open('/tmp/browser.log', 'a+') as file: - proc = subprocess.Popen(args, stdout=file, stderr=file) - return proc - - @staticmethod - def terminate_browser(proc: subprocess.Popen, args: list[str]) -> None: - logger.debug('Terminating browser process using SIGINT...') - - # Use SIGINT and SIGTERM to end process such that cookies remain saved. - proc.send_signal(signal.SIGINT) - proc.send_signal(signal.SIGTERM) - - try: - stdout, stderr = proc.communicate(timeout=5) - except subprocess.TimeoutExpired: - logger.info('Browser process did not terminate after 5s. Killing process through pkill...') - subprocess.run(['pkill', '-2', args[0].split('/')[-1]]) - - proc.wait() - logger.debug('Browser process terminated.') diff --git a/bci/browser/binary/vendors/chromium.py b/bci/browser/binary/vendors/chromium.py deleted file mode 100644 index 4a6a5689..00000000 --- a/bci/browser/binary/vendors/chromium.py +++ /dev/null @@ -1,125 +0,0 @@ -import logging -import os -import re - -from bci import cli, util -from bci.browser.binary.artisanal_manager import ArtisanalBuildManager -from bci.browser.binary.binary import Binary -from bci.database.mongo.binary_cache import BinaryCache -from bci.version_control.states.state import State - -logger = logging.getLogger(__name__) - -EXECUTABLE_NAME = 'chrome' -BIN_FOLDER_PATH = '/app/browser/binaries/chromium' -EXTENSION_FOLDER_PATH = '/app/browser/extensions/chromium' - - -class ChromiumBinary(Binary): - def __init__(self, state: State): - super().__init__(state) - - def save_browser_binary(self, binary_file): - binary_file.save(self.get_bin_path()) - - @property - def executable_name(self) -> str: - return EXECUTABLE_NAME - - @property - def browser_name(self) -> str: - return "chromium" - - @property - def bin_folder_path(self) -> str: - return BIN_FOLDER_PATH - - # Downloadable binaries - - def configure_binary(self): - binary_folder = os.path.dirname(self.get_potential_bin_path()) - self.__remove_unnecessary_files(binary_folder) - cli.execute_and_return_status(f'chmod -R a+x {binary_folder}') - - def __remove_unnecessary_files(self, binary_folder_path: str) -> None: - """ - Remove binary files that are not necessary for default usage of the browser. - This is to improve performance, especially when caching binary files. - - :param binary_folder_path: Path to the folder where the binary files are stored. - """ - locales_folder_path = os.path.join(binary_folder_path, 'locales') - if os.path.isdir(locales_folder_path): - util.remove_all_in_folder(locales_folder_path, except_files=['en-GB.pak', 'en-US.pak']) - - def _get_version(self) -> str: - command = "./chrome --version" - if bin_path := self.get_bin_path(): - output = cli.execute_and_return_output(command, cwd=os.path.dirname(bin_path)) - else: - BinaryCache.remove_binary_files(self.state) - raise AttributeError(f'Could not get binary path for {self.state}') - match = re.match(r'Chromium (?P[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)', output) - if match: - return match.group("version") - raise AttributeError("Could not determine version of binary at '%s'. Version output: %s" % (bin_path, output)) - - @staticmethod - def list_downloaded_binaries() -> list[dict[str, str]]: - return Binary._list_downloaded_binaries(BIN_FOLDER_PATH) - - @staticmethod - def get_artisanal_manager() -> ArtisanalBuildManager: - return Binary._get_artisanal_manager(BIN_FOLDER_PATH, EXECUTABLE_NAME) - - browser_version_to_driver_version = { - '88': "88.0.4324.96", - '87': "87.0.4280.88", - '86': "86.0.4240.22", - '85': "85.0.4183.87", - '84': "84.0.4147.30", - '83': "83.0.4103.39", - '82': "81.0.4044.69", # No chromium driver released for 82 - '81': "81.0.4044.69", - '80': "80.0.3987.16", # 80.0.3987.16 80.0.3987.106 - '79': "79.0.3945.36", - '78': "78.0.3904.11", - '77': "77.0.3865.40", - '76': "76.0.3809.126", - '75': "75.0.3770.8", - '74': "74.0.3729.6", - '73': "73.0.3683.68", - '72': "72.0.3626.7", - '71': "71.0.3578.80", - '70': "70.0.3538.97", - '69': "2.42.591071", - '68': "2.41.578700", - '67': "2.40.565383", - '66': "2.38.552522", - '65': "2.37.544315", - '64': "2.36.540471", - '63': "2.35.528139", - '62': "2.34.522913", - '61': "2.33.506092", - '60': "2.32.498513", - '59': "2.31.488763", # From here on not working - '58': "2.29.461571", - '57': "2.29.461571", - '56': "2.29.461571", - '55': "2.27.440175", - '54': "2.23.409687", - '53': "2.23.409687", # Based on Selenoid https://aerokube.com/images/latest/ - '52': "2.23.409687", # Based on Selenoid - '51': "2.22.397932", # Tried also with 2.21 and 2.23, to no avail - '50': "2.22.397932", # Based on Selenoid - '49': "2.21.371461", # Based on Selenoid - '48': "2.21.371461", - '47': "2.21.371461", - '46': "2.20.353124", - '45': "2.20.353124", - '44': "2.19.346067", - '43': "2.19.346067", - '42': "2.18.343837", - '41': "2.18.343837", - '40': "2.18.343837", - } diff --git a/bci/browser/binary/vendors/firefox.py b/bci/browser/binary/vendors/firefox.py deleted file mode 100644 index 127566c9..00000000 --- a/bci/browser/binary/vendors/firefox.py +++ /dev/null @@ -1,107 +0,0 @@ -import logging -import os -import re - -from bci import cli -from bci.browser.binary.artisanal_manager import ArtisanalBuildManager -from bci.browser.binary.binary import Binary -from bci.version_control.states.state import State - -logger = logging.getLogger('bci') - -EXECUTABLE_NAME = 'firefox' -BIN_FOLDER_PATH = '/app/browser/binaries/firefox' -EXTENSION_FOLDER_PATH = '/app/browser/extensions/firefox' - - -class FirefoxBinary(Binary): - def __init__(self, state: State): - super().__init__(state) - - @property - def executable_name(self) -> str: - return "firefox" - - @property - def browser_name(self) -> str: - return "firefox" - - @property - def bin_folder_path(self) -> str: - return BIN_FOLDER_PATH - - def configure_binary(self) -> None: - binary_folder = os.path.dirname(self.get_potential_bin_path()) - cli.execute_and_return_status(f'chmod -R a+x {binary_folder}') - cli.execute_and_return_status(f'chmod -R a+w {binary_folder}') - # Add policy.json to prevent updating. (this measure is effective from version 60) - # https://github.com/mozilla/policy-templates/blob/master/README.md - # (For earlier versions, the prefs.js file is used) - distributions_path = os.path.join(binary_folder, 'distribution') - os.makedirs(distributions_path, exist_ok=True) - policies_path = os.path.join(distributions_path, 'policies.json') - with open(policies_path, 'a') as file: - file.write('{ "policies": { "DisableAppUpdate": true } }') - - def _get_version(self): - if (bin_path := self.get_bin_path()) is None: - raise AttributeError(f"Binary not available for {self.browser_name} {self.state}") - command = "./firefox --version" - output = cli.execute_and_return_output(command, cwd=os.path.dirname(bin_path)) - match = re.match(r'Mozilla Firefox (?P[0-9]+)\.[0-9]+.*', output) - if match: - return match.group("version") - raise AttributeError( - "Could not determine version of binary at '%s'. Version output: %s" % (bin_path, output)) - - def get_driver_version(self, browser_version): - if browser_version not in self.browser_version_to_driver_version.keys(): - raise AttributeError( - "Could not determine driver version associated with Firefox version %s" % browser_version) - return self.browser_version_to_driver_version[browser_version] - - @staticmethod - def list_downloaded_binaries() -> list[dict[str, str]]: - return Binary._list_downloaded_binaries(BIN_FOLDER_PATH) - - @staticmethod - def get_artisanal_manager() -> ArtisanalBuildManager: - return Binary._get_artisanal_manager(BIN_FOLDER_PATH, EXECUTABLE_NAME) - - browser_version_to_driver_version = { - '84': "0.28.0", - '83': "0.28.0", - '82': "0.27.0", - '81': "0.27.0", - '80': "0.27.0", - '79': "0.27.0", - '78': "0.27.0", - '77': "0.27.0", - '76': "0.27.0", - '75': "0.27.0", - '74': "0.27.0", - '73': "0.27.0", - '72': "0.27.0", - '71': "0.27.0", - '70': "0.27.0", - '69': "0.27.0", - '68': "0.26.0", - '67': "0.26.0", - '66': "0.26.0", - '65': "0.25.0", - '64': "0.26.0", - '63': "0.26.0", - '62': "0.26.0", - '61': "0.26.0", - '60': "0.26.0", - '59': "0.25.0", - '58': "0.20.1", - '57': "0.20.1", - '56': "0.19.1", - '55': "0.20.1", - '54': "0.17.0", - '53': "0.16.1", - '52': "0.15.0", - '51': "0.15.0", - '50': "0.15.0" - } diff --git a/bci/browser/statistics.py b/bci/browser/statistics.py deleted file mode 100644 index 9471dbf3..00000000 --- a/bci/browser/statistics.py +++ /dev/null @@ -1,39 +0,0 @@ -def find_biggest_unavailability_gap(): - revision_nb_mapping = REVISION_NUMBER_MAPPING - - revision_nbs = [int(rev) for rev in revision_nb_mapping.keys()] - revision_nbs.sort() - # Find the biggest gap between two revision numbers - biggest_gap = 0 - biggest_gap_start = None - biggest_gap_end = None - for i in range(1, len(revision_nbs)): - start = revision_nbs[i - 1] - end = revision_nbs[i] - gap = end - start - print(f'{i}: {start} - {end} = {gap}') - if gap > biggest_gap: - biggest_gap = gap - biggest_gap_start = start - biggest_gap_end = end - - print(f"The biggest gap is between revision numbers {biggest_gap_start} and {biggest_gap_end} with a gap of {biggest_gap}") - - -def get_nb_of_revisions_per_major_version(): - binary_availability = BINARY_AVAILABILITY_MAPPING - - major_versions = {} - for item in binary_availability.values(): - major_version = item['app_version'] - if major_version not in major_versions: - major_versions[major_version] = {item['revision_number']} - major_versions[major_version].add(item['revision_number']) - - for major_version, nb_of_revisions in major_versions.items(): - print(f'Major version {major_version}: {len(nb_of_revisions)} revisions (%)') - - -if __name__ == '__main__': - find_biggest_unavailability_gap() - get_nb_of_revisions_per_major_version() diff --git a/bci/browser/support.py b/bci/browser/support.py deleted file mode 100644 index a122b260..00000000 --- a/bci/browser/support.py +++ /dev/null @@ -1,23 +0,0 @@ -import dataclasses - -import bci.version_control.repository.online.chromium as chromium_repo -import bci.version_control.repository.online.firefox as firefox_repo - -from bci.browser.configuration import chromium, firefox - - -def get_chromium_support() -> dict: - return { - 'name': 'chromium', - 'min_version': 20, - 'max_version': chromium_repo.get_most_recent_major_version(), - 'options': [dataclasses.asdict(option) for option in chromium.SUPPORTED_OPTIONS] - } - -def get_firefox_support() -> dict: - return { - 'name': 'firefox', - 'min_version': 20, - 'max_version': firefox_repo.get_most_recent_major_version(), - 'options': [dataclasses.asdict(option) for option in firefox.SUPPORTED_OPTIONS] - } diff --git a/bci/database/mongo/revision_cache.py b/bci/database/mongo/revision_cache.py deleted file mode 100644 index 6c511d11..00000000 --- a/bci/database/mongo/revision_cache.py +++ /dev/null @@ -1,139 +0,0 @@ -import logging -from concurrent.futures import ThreadPoolExecutor -from typing import Optional - -from pymongo import ASCENDING, DESCENDING - -from bci import util -from bci.database.mongo.mongodb import MongoDB - -logger = logging.getLogger(__name__) - -BASE_URL = 'https://bughog.distrinet-research.be/' - - -class RevisionCache: - - @staticmethod - def update() -> None: - def safe_request_json_and_update(collection_name: str, transform=lambda x: x): - url = BASE_URL + collection_name + '.json' - try: - result = util.request_json(url)['data'] - if result is not None: - RevisionCache.__update_collection(collection_name, transform(result)) - except util.ResourceNotFound: - logger.warning(f'Could not update revision cache with resource at {url}') - except Exception: - logger.error(f'Could not update revision cache for {collection_name}', exc_info=True) - - executor = ThreadPoolExecutor() - executor.submit(safe_request_json_and_update, 'firefox_binary_availability', transform=lambda x: list(x.values())) - executor.submit(safe_request_json_and_update, 'firefox_release_base_revs') - executor.submit(safe_request_json_and_update, 'chromium_release_base_revs') - executor.shutdown(wait=False) - - @staticmethod - def __update_collection(collection_name: str, data: list) -> None: - collection = MongoDB().get_collection(collection_name) - if (n := len(data)) == collection.count_documents({}): - logger.debug(f'{collection_name} is still up-to-date ({n} documents).') - else: - collection.delete_many({}) - collection.insert_many(data) - logger.info(f'{collection_name} is updated ({len(data)} documents).') - - @staticmethod - def firefox_get_revision_number(revision_id: str) -> int: - collection = MongoDB().get_collection('firefox_binary_availability') - result = collection.find_one({'revision_id': revision_id}, {'revision_number': 1}) - if result is None or 'revision_number' not in result: - raise AttributeError(f"Could not find 'revision_number' in {result}") - return result['revision_number'] - - @staticmethod - def firefox_has_binary_for(revision_nb: Optional[int], revision_id: Optional[str]) -> bool: - collection = MongoDB().get_collection('firefox_binary_availability') - if revision_nb: - result = collection.find_one({'revision_number': revision_nb}) - elif revision_id: - result = collection.find_one({'revision_number': revision_nb}) - else: - raise AttributeError('No revision number or id was provided') - return result is not None - - @staticmethod - def firefox_get_binary_info(revision_id: str) -> Optional[dict]: - collection = MongoDB().get_collection('firefox_binary_availability') - return collection.find_one({'node': revision_id}, {'files_url': 1, 'app_version': 1}) - - @staticmethod - def firefox_get_previous_and_next_revision_nb_with_binary(revision_nb: int) -> tuple[Optional[int], Optional[int]]: - collection = MongoDB().get_collection('firefox_binary_availability') - - previous_revision_nbs = collection.find({'revision_number': {'$lt': revision_nb}}).sort( - {'revision_number': DESCENDING} - ) - previous_document = next(previous_revision_nbs, None) - - next_revision_nbs = collection.find({'revision_number': {'$gt': revision_nb}}).sort( - {'revision_number': ASCENDING} - ) - next_document = next(next_revision_nbs, None) - - return ( - previous_document['revision_number'] if previous_document else None, - next_document['revision_number'] if next_document else None, - ) - - @staticmethod - def firefox_get_revision_id(revision_nb: int) -> Optional[str]: - collection = MongoDB().get_collection('firefox_binary_availability') - result = collection.find_one({'revision_number': revision_nb}) - if result is None: - return None - return result.get('node', None) - - @staticmethod - def __get_release_base_rev_collection(browser: str) -> str: - match browser: - case 'chromium': - return 'chromium_release_base_revs' - case 'firefox': - return 'firefox_release_base_revs' - case _: - raise AttributeError(f'Could not get collection for browser {browser}') - - @staticmethod - def is_tag(browser: str, tag: str) -> bool: - collection = MongoDB().get_collection(RevisionCache.__get_release_base_rev_collection(browser)) - n = collection.count_documents({'release_tag': tag}) - return n > 0 - - @staticmethod - def get_release_tag(browser: str, major_release_version: int) -> str: - collection = MongoDB().get_collection(RevisionCache.__get_release_base_rev_collection(browser)) - if doc := collection.find_one({'major_version': major_release_version}): - return doc['release_tag'] - raise AttributeError(f"Could not find release tag associated with version '{major_release_version}'") - - @staticmethod - def get_release_revision_number(browser: str, major_release_version: int) -> int: - collection = MongoDB().get_collection(RevisionCache.__get_release_base_rev_collection(browser)) - if doc := collection.find_one({'major_version': major_release_version}): - return doc['revision_number'] - raise AttributeError(f"Could not find major release version '{major_release_version}'") - - @staticmethod - def get_release_revision_id(browser: str, major_release_version: int) -> int: - collection = MongoDB().get_collection(RevisionCache.__get_release_base_rev_collection(browser)) - if doc := collection.find_one({'major_version': major_release_version}): - return doc['revision_id'] - raise AttributeError(f"Could not find major release version '{major_release_version}'") - - @staticmethod - def get_most_recent_major_version(browser:str) -> int: - collection = MongoDB().get_collection(RevisionCache.__get_release_base_rev_collection(browser)) - if doc := collection.find_one(sort=[('major_version', -1)]): - return doc['major_version'] - raise AttributeError("Could not find most recent major release version") diff --git a/bci/evaluations/collectors/collector.py b/bci/evaluations/collectors/collector.py deleted file mode 100644 index b67be5ff..00000000 --- a/bci/evaluations/collectors/collector.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging -from enum import Enum - -from bci.evaluations.collectors.base import BaseCollector - -from .logs import LogCollector -from .requests import RequestCollector - -logger = logging.getLogger(__name__) - - -class Type(Enum): - REQUESTS = 1 - LOGS = 2 - - -class Collector: - def __init__(self, types: list[Type]) -> None: - self.collectors: list[BaseCollector] = [] - if Type.REQUESTS in types: - collector = RequestCollector() - self.collectors.append(collector) - if Type.LOGS in types: - collector = LogCollector() - self.collectors.append(collector) - logger.debug(f'Using {len(self.collectors)} result collectors') - - def start(self): - for collector in self.collectors: - collector.start() - - def stop(self): - for collector in self.collectors: - collector.stop() - - def collect_results(self) -> dict: - all_data = {} - for collector in self.collectors: - collector.parse_data() - all_data.update(collector.data) - logger.debug(f'Collected data: {all_data}') - return all_data diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py deleted file mode 100644 index f2ad6a33..00000000 --- a/bci/evaluations/custom/custom_evaluation.py +++ /dev/null @@ -1,276 +0,0 @@ -import logging -import os -from typing import Optional - -from bci.browser.configuration.browser import Browser -from bci.browser.interaction.interaction import Interaction -from bci.configuration import Global -from bci.evaluations.collectors.collector import Collector, Type -from bci.evaluations.evaluation_framework import EvaluationFramework, FailedSanityCheck -from bci.evaluations.logic import TestParameters, TestResult -from bci.version_control.state_result_factory import StateResultFactory -from bci.web.clients import Clients - -logger = logging.getLogger(__name__) - - -class CustomEvaluationFramework(EvaluationFramework): - __files_and_folders_to_ignore = ['.DS_Store'] - - def __init__(self): - super().__init__() - self.dir_tree = self.initialize_dir_tree() - self.tests_per_project = self.initialize_tests_and_interactions(self.dir_tree) - - @staticmethod - def initialize_dir_tree() -> dict: - """ - Initializes directory tree of experiments. - """ - path = Global.custom_page_folder - dir_tree = {} - - def set_nested_value(d: dict, keys: list[str], value: dict): - nested_dict = d - for key in keys[:-1]: - nested_dict = nested_dict[key] - nested_dict[keys[-1]] = value - - for root, dirs, files in os.walk(path): - # Remove base path from root - root = root[len(path) :] - keys = root.split('/')[1:] - subdir_tree = { - dir: {} for dir in dirs if dir not in CustomEvaluationFramework.__files_and_folders_to_ignore - } | {file: None for file in files if file not in CustomEvaluationFramework.__files_and_folders_to_ignore} - if root: - set_nested_value(dir_tree, keys, subdir_tree) - else: - dir_tree = subdir_tree - - return dir_tree - - @staticmethod - def initialize_tests_and_interactions(dir_tree: dict) -> dict: - experiments_per_project = {} - page_folder_path = Global.custom_page_folder - for project, experiments in dir_tree.items(): - # Find tests in folder - project_path = os.path.join(page_folder_path, project) - experiments_per_project[project] = {} - for experiment in experiments: - data = {} - - if interaction_script := CustomEvaluationFramework.__get_interaction_script(project_path, experiment): - data['script'] = interaction_script - elif url_queue := CustomEvaluationFramework.__get_url_queue(project, project_path, experiment): - data['url_queue'] = url_queue - - data['runnable'] = CustomEvaluationFramework.is_runnable_experiment(project, experiment, dir_tree, data) - - experiments_per_project[project][experiment] = data - return experiments_per_project - - @staticmethod - def __get_interaction_script(project_path: str, experiment: str) -> list[str] | None: - interaction_file_path = os.path.join(project_path, experiment, 'script.cmd') - if os.path.isfile(interaction_file_path): - # If an interaction script is specified, it is parsed and used - with open(interaction_file_path) as file: - return file.readlines() - return None - - @staticmethod - def __get_url_queue(project: str, project_path: str, experiment: str) -> Optional[list[str]]: - url_queue_file_path = os.path.join(project_path, experiment, 'url_queue.txt') - if os.path.isfile(url_queue_file_path): - # If an URL queue is specified, it is parsed and used - with open(url_queue_file_path) as file: - return file.readlines() - else: - # Otherwise, a default URL queue is used, based on the domain that hosts the main page - experiment_path = os.path.join(project_path, experiment) - assert os.path.isdir(experiment_path) - for domain in os.listdir(experiment_path): - main_folder_path = os.path.join(experiment_path, domain, 'main') - if os.path.exists(main_folder_path): - return [ - f'https://{domain}/{project}/{experiment}/main', - 'https://a.test/report/?bughog_sanity_check=OK', - ] - return None - - @staticmethod - def is_runnable_experiment(project: str, poc: str, dir_tree: dict[str, dict], data: dict[str, str]) -> bool: - # Always runnable if there is either an interaction script or url_queue present - if 'script' in data or 'url_queue' in data: - return True - - # Should have exactly one main folder otherwise - domains = dir_tree[project][poc] - main_paths = [paths for paths in domains.values() if paths is not None and 'main' in paths.keys()] - if len(main_paths) != 1: - return False - # Main should have index.html - if 'index.html' not in main_paths[0]['main'].keys(): - return False - return True - - def perform_specific_evaluation(self, browser: Browser, params: TestParameters) -> TestResult: - logger.info(f'Starting test for {params}') - browser_version = browser.version - binary_origin = browser.get_binary_origin() - - state_result_factory = StateResultFactory(experiment=params.mech_group) - collector = Collector([Type.REQUESTS, Type.LOGS]) - collector.start() - - is_dirty = False - tries_left = 3 - experiment = self.tests_per_project[params.evaluation_configuration.project][params.mech_group] - try: - sanity_check_was_successful = False - poc_was_reproduced = False - while not poc_was_reproduced and tries_left > 0: - tries_left -= 1 - browser.pre_try_setup() - if 'script' in experiment: - interaction = Interaction(browser, experiment['script'], params) - interaction.execute() - else: - url_queue = experiment['url_queue'] - for url in url_queue: - browser.visit(url) - browser.post_try_cleanup() - intermediary_state_result = state_result_factory.get_result(collector.collect_results()) - sanity_check_was_successful |= not intermediary_state_result.is_dirty - poc_was_reproduced = intermediary_state_result.reproduced - if not poc_was_reproduced and not sanity_check_was_successful: - raise FailedSanityCheck() - except FailedSanityCheck: - logger.error('Evaluation sanity check has failed', exc_info=True) - is_dirty = True - except Exception as e: - logger.error(f'An error during evaluation: {e}', exc_info=True) - is_dirty = True - finally: - logger.debug(f'Evaluation finished with {tries_left} tries left') - collector.stop() - results = collector.collect_results() - return params.create_test_result_with(browser_version, binary_origin, results, is_dirty) - - def get_mech_groups(self, project: str) -> list[tuple[str, bool]]: - if project not in self.tests_per_project: - return [] - pocs = [(poc_name, poc_data['runnable']) for poc_name, poc_data in self.tests_per_project[project].items()] - return sorted(pocs, key=lambda x: x[0]) - - def get_projects(self) -> list[str]: - return sorted(list(self.tests_per_project.keys())) - - def create_empty_project(self, project_name: str): - self.is_valid_name(project_name) - if project_name in self.dir_tree: - raise AttributeError(f"The given project name '{project_name}' already exists.") - - new_project_path = os.path.join(Global.custom_page_folder, project_name) - os.mkdir(new_project_path) - self.sync_with_folders() - - def get_poc_structure(self, project: str, poc: str) -> dict: - return self.dir_tree[project][poc] - - def _get_poc_file_path(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str: - # Top-level config file - if domain == 'Config' and path == '_': - return os.path.join(Global.custom_page_folder, project, poc, file_name) - - return os.path.join(Global.custom_page_folder, project, poc, domain, path, file_name) - - def get_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str: - file_path = self._get_poc_file_path(project, poc, domain, path, file_name) - if os.path.isfile(file_path): - with open(file_path) as file: - return file.read() - raise AttributeError(f"Could not find PoC file at expected path '{file_path}'") - - def update_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str, content: str) -> bool: - file_path = self._get_poc_file_path(project, poc, domain, path, file_name) - if os.path.isfile(file_path): - if content == '': - logger.warning('Attempt to save empty file ignored') - return False - with open(file_path, 'w') as file: - file.write(content) - return True - return False - - def create_empty_poc(self, project: str, poc_name: str): - self.is_valid_name(poc_name) - poc_path = os.path.join(Global.custom_page_folder, project, poc_name) - if os.path.exists(poc_path): - raise AttributeError(f"The given PoC name '{poc_name}' already exists.") - - os.makedirs(poc_path) - self.sync_with_folders() - Clients.push_experiments_to_all() - - def add_page(self, project: str, poc: str, domain: str, path: str, file_type: str): - domain_path = os.path.join(Global.custom_page_folder, project, poc, domain) - if not os.path.exists(domain_path): - os.makedirs(domain_path) - - self.is_valid_name(path) - if file_type == 'py': - file_name = path if path.endswith('.py') else path + '.py' - file_path = os.path.join(domain_path, file_name) - else: - page_path = os.path.join(domain_path, path) - if not os.path.exists(page_path): - os.makedirs(page_path) - new_file_name = f'index.{file_type}' - file_path = os.path.join(page_path, new_file_name) - headers_file_path = os.path.join(page_path, 'headers.json') - if not os.path.exists(headers_file_path): - with open(headers_file_path, 'w') as file: - file.write(self.get_default_file_content('headers.json')) - - if os.path.exists(file_path): - raise AttributeError(f"The given page '{path}' does already exist.") - with open(file_path, 'w') as file: - file.write(self.get_default_file_content(file_type)) - - self.sync_with_folders() - # Notify clients of change (an experiment might now be runnable) - Clients.push_experiments_to_all() - - def add_config(self, project: str, poc: str, type: str) -> bool: - content = self.get_default_file_content(type) - - if content == '': - return False - - file_path = os.path.join(Global.custom_page_folder, project, poc, type) - with open(file_path, 'w') as file: - file.write(content) - - self.sync_with_folders() - # Notify clients of change (an experiment might now be runnable) - Clients.push_experiments_to_all() - - return True - - @staticmethod - def get_default_file_content(file_type: str) -> str: - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'default_files/{file_type}') - - if not os.path.exists(path): - return '' - - with open(path, 'r') as file: - return file.read() - - def sync_with_folders(self): - self.dir_tree = self.initialize_dir_tree() - self.tests_per_project = self.initialize_tests_and_interactions(self.dir_tree) - logger.info('Framework is synced with folders') diff --git a/bci/evaluations/evaluation_framework.py b/bci/evaluations/evaluation_framework.py deleted file mode 100644 index 118bce31..00000000 --- a/bci/evaluations/evaluation_framework.py +++ /dev/null @@ -1,93 +0,0 @@ -import logging -import os -import re -from abc import ABC, abstractmethod - -from bci.browser.configuration.browser import Browser -from bci.configuration import Global -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import TestParameters, TestResult, WorkerParameters - -logger = logging.getLogger(__name__) - - -class EvaluationFramework(ABC): - def __init__(self): - self.should_stop = False - - def evaluate(self, worker_params: WorkerParameters, is_worker=False): - test_params = worker_params.create_test_params() - - if MongoDB().has_result(test_params): - logger.warning( - f"Experiment '{test_params.mech_group}' for '{test_params.state}' was already performed, skipping." - ) - return - - browser_config = worker_params.browser_configuration - eval_config = worker_params.evaluation_configuration - state = worker_params.state - browser = Browser.get_browser(browser_config, eval_config, state) - browser.pre_evaluation_setup() - - if self.should_stop: - self.should_stop = False - return - try: - browser.pre_test_setup() - result = self.perform_specific_evaluation(browser, test_params) - MongoDB().store_result(result) - logger.info(f'Test finalized: {test_params}') - except Exception as e: - state.failed_by_error = True - if is_worker: - raise e - else: - logger.error('An error occurred during evaluation', exc_info=True) - finally: - browser.post_test_cleanup() - - browser.post_evaluation_cleanup() - logger.debug('Evaluation finished') - - @abstractmethod - def perform_specific_evaluation(self, browser: Browser, params: TestParameters) -> TestResult: - pass - - @staticmethod - def get_extension_path(browser: str, extension_file: str): - folder_path = Global.get_extension_folder(browser) - path = os.path.join(folder_path, extension_file) - if not os.path.isfile(path): - raise AttributeError("Could not find file '%s'" % path) - return path - - def stop_gracefully(self): - self.should_stop = True - - @abstractmethod - def get_mech_groups(self, project: str) -> list[tuple[str, bool]]: - """ - Returns the available mechanism groups for this evaluation framework. - """ - pass - - @staticmethod - def is_valid_name(name: str) -> None: - """ - Checks whether the given string is a valid experiment, page or project name, and raises an exception if not. - This is to prevent issues with URL encoding and decoding. - - :param name: Name to be checked on validity. - """ - if name is None or name == '': - raise AttributeError("The given name cannot be empty.") - if re.match(r'^[A-Za-z0-9_\-.]+$', name) is None: - raise AttributeError( - f"The given name '{name}' is invalid. Only letters, numbers, " - "'.', '-' and '_' can be used, and the name should not be empty." - ) - - -class FailedSanityCheck(Exception): - pass diff --git a/bci/evaluations/experiments.py b/bci/evaluations/experiments.py deleted file mode 100644 index e7756d54..00000000 --- a/bci/evaluations/experiments.py +++ /dev/null @@ -1,102 +0,0 @@ -import json -import logging -import os - -logger = logging.getLogger(__name__) - -SUPPORTED_FILE_TYPES = [ - 'css', - 'html', - 'js', - 'py', - 'xml', -] -SUPPORTED_DOMAINS = [ - 'leak.test', - 'a.test', - 'sub.a.test', - 'sub.sub.a.test', - 'b.test', - 'sub.b.test', - 'adition.com', -] -EXPERIMENT_FOLDER_PATH = '/app/experiments/pages' - - -def verify() -> None: - """ - Verifies the experiment pages, logger warnings for unsupported configurations. - """ - for project in os.listdir(EXPERIMENT_FOLDER_PATH): - project_path = os.path.join(EXPERIMENT_FOLDER_PATH, project) - if not os.path.isdir(project_path): - logger.warning(f"Unexpected file in '{__user_path(project_path)}' will be ignored.") - continue - for experiment in os.listdir(project_path): - __verify_experiment(project, experiment) - - -def __verify_experiment(project: str, experiment: str) -> None: - experiment_path = os.path.join(EXPERIMENT_FOLDER_PATH, project, experiment) - if not os.path.isdir(experiment_path): - logger.warning(f"Unexpected file at '{__user_path(experiment_path)}' will be ignored.") - return - for domain in os.listdir(experiment_path): - if domain in ['script.cmd', 'url_queue.txt']: - continue - domain_path = os.path.join(experiment_path, domain) - if not os.path.isdir(domain_path): - logger.warning(f"Unexpected file '{__user_path(domain_path)}' will be ignored.") - continue - if domain not in SUPPORTED_DOMAINS: - logger.warning(f"Unsupported domain '{domain}' in '{__user_path(experiment_path)}' will be ignored.") - for page in os.listdir(domain_path): - __verify_page(project, experiment, domain, page) - - -def __verify_page(project: str, experiment: str, domain: str, page: str) -> None: - page_path = os.path.join(EXPERIMENT_FOLDER_PATH, project, experiment, domain, page) - if page.endswith('.py'): - return - if not os.path.isdir(page_path): - logger.warning(f"Unexpected file at '{__user_path(page_path)}' will be ignored.") - return - for file_name in os.listdir(page_path): - file_path = os.path.join(page_path, file_name) - if not os.path.isfile(file_path): - logger.warning(f"Unexpected folder at '{__user_path(page_path)}' will be ignored.") - continue - if file_name == 'headers.json': - __verify_headers(file_path) - continue - file_name_split = file_name.split('.') - if len(file_name_split) < 2: - logger.warning(f"Could not deduce file extension at '{__user_path(file_path)}'.") - if file_name_split[-1] not in SUPPORTED_FILE_TYPES: - logger.warning(f"File type of '{__user_path(file_path)}' is not supported.") - - -def __verify_headers(path: str) -> None: - """ - Verifies whether the headers file at the given path is valid. - """ - with open(path, 'r', encoding='utf-8') as file: - try: - json_content = json.load(file) - except json.decoder.JSONDecodeError: - logger.warning(f"Could not parse '{__user_path(path)}'") - return - if not isinstance(json_content, list): - raise AttributeError(f"Not a list: '{__user_path(path)}'") - for item in json_content: - if 'key' and 'value' not in item: - logger.warning(f"Not all dictionary entries contain a key-value combination in '{__user_path(path)}'.") - - -def __user_path(path: str) -> str: - """ - Translates the given path to the user readeable path outside container. - """ - if path.startswith('/app/'): - return path[5:] - return path diff --git a/bci/evaluations/logic.py b/bci/evaluations/logic.py deleted file mode 100644 index 0f5042a8..00000000 --- a/bci/evaluations/logic.py +++ /dev/null @@ -1,341 +0,0 @@ -from __future__ import annotations - -import json -import logging -from dataclasses import asdict, dataclass -from typing import Optional - -from werkzeug.datastructures import ImmutableMultiDict - -from bci.version_control.states.state import State - -logger = logging.getLogger(__name__) - - -@dataclass(frozen=True) -class EvaluationParameters: - browser_configuration: BrowserConfiguration - evaluation_configuration: EvaluationConfiguration - evaluation_range: EvaluationRange - sequence_configuration: SequenceConfiguration - database_collection: str - - def create_worker_params_for( - self, state: State, database_connection_params: DatabaseParameters) -> WorkerParameters: - return WorkerParameters( - self.browser_configuration, - self.evaluation_configuration, - state, - self.evaluation_range.mech_group, - self.database_collection, - database_connection_params - ) - - def create_test_for(self, state: State) -> TestParameters: - return TestParameters( - self.browser_configuration, self.evaluation_configuration, state, self.evaluation_range.mech_group, self.database_collection - ) - - def create_plot_params(self, target_mech_id: str, dirty_allowed: bool = True) -> PlotParameters: - return PlotParameters( - self.evaluation_range.mech_group, - target_mech_id, - self.browser_configuration.browser_name, - self.database_collection, - self.evaluation_range.major_version_range, - self.evaluation_range.revision_number_range, - self.browser_configuration.browser_setting, - self.browser_configuration.extensions, - self.browser_configuration.cli_options, - dirty_allowed, - ) - - -@dataclass(frozen=True) -class BrowserConfiguration: - browser_name: str - browser_setting: str - cli_options: list[str] - extensions: list[str] - - def to_dict(self) -> dict: - return asdict(self) - - @staticmethod - def from_dict(data: dict) -> BrowserConfiguration: - return BrowserConfiguration( - data['browser_name'], - data['browser_setting'], - data['cli_options'], - data['extensions'] - ) - - -@dataclass(frozen=True) -class EvaluationConfiguration: - project: str - automation: str - seconds_per_visit: int = 5 - - def to_dict(self) -> dict: - return asdict(self) - - @staticmethod - def from_dict(data: dict) -> EvaluationConfiguration: - return EvaluationConfiguration(data['project'], data['automation'], data['seconds_per_visit']) - - -@dataclass(frozen=True) -class EvaluationRange: - mech_group: str - major_version_range: tuple[int, int] | None = None - revision_number_range: tuple[int, int] | None = None - only_release_revisions: bool = False - - def __post_init__(self): - if self.major_version_range: - assert self.major_version_range[0] <= self.major_version_range[1] - elif self.revision_number_range: - assert self.revision_number_range[0] <= self.revision_number_range[1] - else: - raise AttributeError('Evaluation ranges require either major versions or revision numbers') - - -@dataclass(frozen=True) -class SequenceConfiguration: - nb_of_containers: int = 8 - sequence_limit: int = 10000 - search_strategy: str | None = None - - -@dataclass(frozen=True) -class DatabaseParameters: - host: str - username: str - password: str - database_name: str - binary_cache_limit: int - - def to_dict(self) -> dict: - return asdict(self) - - @staticmethod - def from_dict(data: dict) -> DatabaseParameters: - return DatabaseParameters(data['host'], data['username'], data['password'], data['database_name'], data['binary_cache_limit']) - - def __str__(self) -> str: - return f'{self.username}@{self.host}:27017/{self.database_name}' - - -@dataclass(frozen=True) -class WorkerParameters: - browser_configuration: BrowserConfiguration - evaluation_configuration: EvaluationConfiguration - state: State - mech_group: str - database_collection: str - database_connection_params: DatabaseParameters - - def create_test_params(self) -> TestParameters: - return TestParameters( - self.browser_configuration, self.evaluation_configuration, self.state, self.mech_group, self.database_collection - ) - - def _to_dict(self): - return { - 'browser_configuration': self.browser_configuration.to_dict(), - 'evaluation_configuration': self.evaluation_configuration.to_dict(), - 'state': self.state.to_dict(), - 'mech_group': self.mech_group, - 'database_collection': self.database_collection, - 'database_connection_params': self.database_connection_params.to_dict() - } - - def serialize(self) -> str: - return json.dumps(self._to_dict()) - - def __repr__(self) -> str: - param_dict = self._to_dict() - # Mask password - param_dict['database_connection_params']['password'] = '*' - return json.dumps(param_dict) - - @staticmethod - def get_database_params(string: str) -> DatabaseParameters: - data = json.loads(string) - return DatabaseParameters.from_dict(data['database_connection_params']) - - @staticmethod - def deserialize(string: str) -> WorkerParameters: - data = json.loads(string) - browser_config = BrowserConfiguration.from_dict(data['browser_configuration']) - eval_config = EvaluationConfiguration.from_dict(data['evaluation_configuration']) - state = State.from_dict(data['state']) - mech_group = data['mech_group'] - database_collection = data['database_collection'] - database_connection_params = DatabaseParameters.from_dict(data['database_connection_params']) - return WorkerParameters( - browser_config, eval_config, state, mech_group, database_collection, database_connection_params - ) - - def __str__(self) -> str: - return f'Eval({self.state}: [{", ".join(self.mech_group)}])' - - -@dataclass(frozen=True) -class TestParameters: - browser_configuration: BrowserConfiguration - evaluation_configuration: EvaluationConfiguration - state: State - mech_group: str - database_collection: str - - def create_test_result_with(self, browser_version: str, binary_origin: str, data: dict, dirty: bool) -> TestResult: - return TestResult(self, browser_version, binary_origin, data, dirty) - - @staticmethod - def from_dict(data) -> Optional[TestParameters]: - if data is None: - return None - browser_configuration = BrowserConfiguration.from_dict(data) - evaluation_configuration = EvaluationConfiguration.from_dict(data) - state = State.from_dict(data) - mech_group = data['mech_group'] - database_collection = data['db_collection'] - return TestParameters(browser_configuration, evaluation_configuration, state, mech_group, database_collection) - -@dataclass(frozen=True) -class TestResult: - params: TestParameters - browser_version: str - binary_origin: str - data: dict - is_dirty: bool = False - driver_version: str | None = None - - @property - def padded_browser_version(self): - padding_target = 4 - padded_version = [] - for sub in self.browser_version.split('.'): - if len(sub) > padding_target: - raise AttributeError(f"Version '{self.browser_version}' is too big to be padded") - padded_version.append('0' * (padding_target - len(sub)) + sub) - return '.'.join(padded_version) - - -@dataclass(frozen=True) -class PlotParameters: - mech_group: Optional[str] - target_mech_id: Optional[str] - browser_name: Optional[str] - database_collection: Optional[str] - major_version_range: Optional[tuple[int,int]] = None - revision_number_range: Optional[tuple[int,int]] = None - browser_config: str = 'default' - extensions: Optional[list[str]] = None - cli_options: Optional[list[str]] = None - dirty_allowed: bool = True - - @staticmethod - def from_dict(data: dict) -> PlotParameters: - if data.get("lower_version", None) and data.get("upper_version", None): - major_version_range = (data["lower_version"], data["upper_version"]) - else: - major_version_range = None - if data.get("lower_revision_nb", None) and data.get("upper_revision_nb", None): - revision_number_range = ( - data["lower_revision_nb"], - data["upper_revision_nb"], - ) - else: - revision_number_range = None - return PlotParameters( - data.get('plot_mech_group', None), - data.get('target_mech_id', None), - data.get('browser_name', None), - data.get('db_collection', None), - major_version_range=major_version_range, - revision_number_range=revision_number_range, - browser_config=data.get("browser_setting", "default"), - extensions=data.get("extensions", []), - cli_options=data.get("cli_options", []), - dirty_allowed=data.get("dirty_allowed", True), - ) - - -@staticmethod -def evaluation_factory(kwargs: ImmutableMultiDict) -> list[EvaluationParameters]: - mech_groups = kwargs.get('tests') - if mech_groups is None: - raise MissingParametersException() - - browser_configuration = BrowserConfiguration.from_dict(kwargs) - evaluation_configuration = EvaluationConfiguration( - kwargs['project'], kwargs['automation'], int(kwargs.get('seconds_per_visit', 5)) - ) - sequence_configuration = SequenceConfiguration( - int(kwargs.get('nb_of_containers')), - int(kwargs.get('sequence_limit')), - kwargs.get('search_strategy'), - ) - evaluation_params_list = [] - for mech_group in mech_groups: - evaluation_range = EvaluationRange( - mech_group, - __get_version_range(kwargs), - __get_revision_number_range(kwargs), - kwargs.get('only_release_revisions', False), - ) - database_collection = kwargs.get('db_collection') - evaluation_params = EvaluationParameters( - browser_configuration, evaluation_configuration, evaluation_range, sequence_configuration, database_collection - ) - evaluation_params_list.append(evaluation_params) - return evaluation_params_list - - -@staticmethod -def __get_cookie_name(form_data: dict[str, str]) -> str | None: - if form_data['check_for'] == 'request': - return None - if 'cookie_name' in form_data: - return form_data['cookie_name'] - return 'generic' - - -@staticmethod -def __get_version_range(form_data: dict[str, str]) -> tuple[int, int] | None: - lower_version = form_data.get('lower_version', None) - upper_version = form_data.get('upper_version', None) - lower_version = int(lower_version) if lower_version else None - upper_version = int(upper_version) if upper_version else None - assert (lower_version is None) == (upper_version is None) - return (lower_version, upper_version) if lower_version is not None else None - - -@staticmethod -def __get_revision_number_range(form_data: dict[str, str]) -> tuple[int, int] | None: - lower_rev_number = form_data.get('lower_revision_nb', None) - upper_rev_number = form_data.get('upper_revision_nb', None) - lower_rev_number = int(lower_rev_number) if lower_rev_number else None - upper_rev_number = int(upper_rev_number) if upper_rev_number else None - assert (lower_rev_number is None) == (upper_rev_number is None) - return (lower_rev_number, upper_rev_number) if lower_rev_number is not None else None - - -@staticmethod -def __get_extensions(form_data: dict[str, str]) -> list[str]: - return list( - map( - lambda x: x.replace('ext_', ''), - filter( - lambda x: x.startswith('ext_') and form_data[x] == 'true', - form_data.keys(), - ), - ) - ) - - -class MissingParametersException(Exception): - pass diff --git a/bci/version_control/repository/online/chromium.py b/bci/version_control/repository/online/chromium.py deleted file mode 100644 index af569fe1..00000000 --- a/bci/version_control/repository/online/chromium.py +++ /dev/null @@ -1,21 +0,0 @@ -from bci.database.mongo.revision_cache import RevisionCache - - -def is_tag(tag: str) -> bool: - return RevisionCache.is_tag('chromium', tag) - - -def get_release_tag(major_release_version: int) -> str: - return RevisionCache.get_release_tag('chromium', major_release_version) - - -def get_release_revision_number(major_release_version: int) -> int: - return RevisionCache.get_release_revision_number('chromium', major_release_version) - - -def get_release_revision_id(major_release_version: int) -> int: - return RevisionCache.get_release_revision_id('chromium', major_release_version) - - -def get_most_recent_major_version() -> int: - return RevisionCache.get_most_recent_major_version('chromium') diff --git a/bci/version_control/repository/online/firefox.py b/bci/version_control/repository/online/firefox.py deleted file mode 100644 index a7dcd486..00000000 --- a/bci/version_control/repository/online/firefox.py +++ /dev/null @@ -1,21 +0,0 @@ -from bci.database.mongo.revision_cache import RevisionCache - - -def is_tag(tag: str) -> bool: - return RevisionCache.is_tag('firefox', tag) - - -def get_release_tag(major_release_version: int) -> str: - return RevisionCache.get_release_tag('firefox', major_release_version) - - -def get_release_revision_number(major_release_version: int) -> int: - return RevisionCache.get_release_revision_number('firefox', major_release_version) - - -def get_release_revision_id(major_release_version: int) -> int: - return RevisionCache.get_release_revision_id('firefox', major_release_version) - - -def get_most_recent_major_version() -> int: - return RevisionCache.get_most_recent_major_version('firefox') diff --git a/bci/version_control/state_result_factory.py b/bci/version_control/state_result_factory.py deleted file mode 100644 index 507e03f9..00000000 --- a/bci/version_control/state_result_factory.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import Optional -from urllib.parse import urlparse - -from bci.version_control.states.state import StateResult - - -class StateResultFactory: - def __init__(self, experiment: Optional[str] = None) -> None: - self.__experiment = experiment - - def get_result(self, state_result_data: dict) -> StateResult: - """ - Returns a StateResult object based on the given state result data. - """ - requests = state_result_data.get('requests', []) - request_vars = state_result_data.get('req_vars', []) - log_vars = state_result_data.get('log_vars', []) - reproduced = self.__is_reproduced(request_vars, log_vars) or self.__is_reproduced_deprecated(requests) - is_dirty = not reproduced and not ( - self.__sanity_check_was_successful(state_result_data) - or self.__sanity_check_was_successful_deprecated(state_result_data) - ) - return StateResult(requests, request_vars, log_vars, is_dirty, reproduced) - - def __sanity_check_was_successful(self, state_result_data: dict) -> bool: - """ - Returns whether the sanity check was successful. - """ - return {'var': 'sanity_check', 'val': 'OK'} in state_result_data['req_vars'] - - def __sanity_check_was_successful_deprecated(self, state_result_data: dict) -> bool: - """ - Returns whether the sanity check was successful based on the leak GET parameter (deprecated). - """ - if self.__experiment is None: - return False - requests_to_report_endpoint = [ - request for request in state_result_data['requests'] if 'report/?leak=baseline' in request['url'] - ] - return len(requests_to_report_endpoint) > 0 - - def __is_reproduced(self, request_vars: list, log_vars: list) -> bool: - """ - Returns whether the PoC is reproduced according to the reproduced variable. - """ - return {'var': 'reproduced', 'val': 'OK'} in request_vars + log_vars - - def __is_reproduced_deprecated(self, requests: dict) -> bool: - """ - Returns whether the PoC is reproduced according to the leak GET parameter (deprecated). - """ - # Because Nginx takes care of all HTTPS traffic, flask (which doubles as proxy) only sees HTTP traffic. - # Browser <--HTTPS--> Nginx <--HTTP--> Flask - if self.__experiment is None: - return False - valid_report_requests = [ - request - for request in requests - if ( - urlparse(request['url']).path in ['/report', '/report/'] - and f'leak={self.__experiment}' in urlparse(request['url']).query - ) - ] - return valid_report_requests != [] diff --git a/bci/version_control/states/revisions/base.py b/bci/version_control/states/revisions/base.py deleted file mode 100644 index 52959a10..00000000 --- a/bci/version_control/states/revisions/base.py +++ /dev/null @@ -1,101 +0,0 @@ -import logging -import re -from abc import abstractmethod -from typing import Optional - -from bci.version_control.states.state import State - -logger = logging.getLogger(__name__) - - -class BaseRevision(State): - def __init__(self, revision_id: Optional[str] = None, revision_nb: Optional[int] = None): - super().__init__() - if revision_id is None and revision_nb is None: - raise AttributeError('A state must be initiliazed with either a revision id or revision number') - - self._revision_id = revision_id - self._revision_nb = revision_nb - self._fetch_missing_data() - - if self._revision_id is not None and not self._is_valid_revision_id(self._revision_id): - raise AttributeError(f"Invalid revision id '{self._revision_id}' for state '{self}'") - - if self._revision_nb is not None and not self._is_valid_revision_number(self._revision_nb): - raise AttributeError(f"Invalid revision number '{self._revision_nb}' for state '{self}'") - - @property - @abstractmethod - def browser_name(self) -> str: - pass - - @property - def name(self) -> str: - return f'{self._revision_nb}' - - @property - def type(self) -> str: - return 'revision' - - @property - def index(self) -> int: - return self._revision_nb - - @property - def revision_nb(self) -> int: - return self._revision_nb - - def to_dict(self) -> dict: - """ - Returns a dictionary representation of the state. - """ - state_dict = {'type': self.type, 'browser_name': self.browser_name} - if self._revision_id: - state_dict['revision_id'] = self._revision_id - if self._revision_nb: - state_dict['revision_number'] = self._revision_nb - return state_dict - - @staticmethod - def from_dict(data: dict) -> State: - from bci.version_control.states.revisions.chromium import ChromiumRevision - from bci.version_control.states.revisions.firefox import FirefoxRevision - - match data['browser_name']: - case 'chromium': - state = ChromiumRevision(revision_id=data.get('revision_id', None), revision_nb=data['revision_number']) - case 'firefox': - state = FirefoxRevision(revision_id=data.get('revision_id', None), revision_nb=data['revision_number']) - case _: - raise Exception(f'Unknown browser: {data["browser_name"]}') - return state - - def _has_revision_id(self) -> bool: - return self._revision_id is not None - - def _has_revision_number(self) -> bool: - return self._revision_nb is not None - - @abstractmethod - def _fetch_missing_data(self): - pass - - def _is_valid_revision_id(self, revision_id: str) -> bool: - """ - Checks if a revision id is valid. - A valid revision id is a 40 character long string containing only lowercase letters and numbers. - """ - return re.match(r'[a-z0-9]{40}', revision_id) is not None - - def _is_valid_revision_number(self, revision_number: int) -> bool: - """ - Checks if a revision number is valid. - A valid revision number is a positive integer. - """ - return re.match(r'[0-9]{1,7}', str(revision_number)) is not None - - def __str__(self): - return f'RevisionState(number: {self._revision_nb}, id: {self._revision_id})' - - def __repr__(self): - return f'RevisionState(number: {self._revision_nb}, id: {self._revision_id})' diff --git a/bci/version_control/states/revisions/chromium.py b/bci/version_control/states/revisions/chromium.py deleted file mode 100644 index 7be60106..00000000 --- a/bci/version_control/states/revisions/chromium.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Optional - -import requests - -from bci.database.mongo.mongodb import MongoDB -from bci.version_control.revision_parser.chromium_parser import ChromiumRevisionParser -from bci.version_control.states.revisions.base import BaseRevision - -PARSER = ChromiumRevisionParser() - - -class ChromiumRevision(BaseRevision): - def __init__(self, revision_id: Optional[str] = None, revision_nb: Optional[int] = None): - super().__init__(revision_id, revision_nb) - - @property - def browser_name(self): - return 'chromium' - - def has_online_binary(self) -> bool: - cached_binary_available_online = MongoDB().has_binary_available_online('chromium', self) - if cached_binary_available_online is not None: - return cached_binary_available_online - url = f'https://www.googleapis.com/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F{self._revision_nb}%2Fchrome-linux.zip' - response = requests.get(url, stream=True) - has_binary_online = response.status_code == 200 - MongoDB().store_binary_availability_online_cache('chromium', self, has_binary_online) - return has_binary_online - - def get_online_binary_urls(self) -> list[str]: - return [( - 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/%s%%2F%s%%2Fchrome-%s.zip?alt=media' - % ('Linux_x64', self._revision_nb, 'linux') - )] - - def _fetch_missing_data(self) -> None: - """ - States are initialized with either a revision id or revision number. - This method attempts to fetch other data to complete this state object. - """ - # First check if the missing data is available in the cache - if self._revision_id and self._revision_nb: - return - if state := MongoDB().get_complete_state_dict_from_binary_availability_cache(self): - if self._revision_id is None: - self._revision_id = state.get('revision_id', None) - if self._revision_nb is None: - self._revision_nb = state.get('revision_number', None) - # If not, fetch the missing data from the parser - if self._revision_id is None: - self._revision_id = PARSER.get_revision_id(self._revision_nb) - if self._revision_nb is None: - self._revision_nb = PARSER.get_revision_nb(self._revision_id) diff --git a/bci/version_control/states/revisions/firefox.py b/bci/version_control/states/revisions/firefox.py deleted file mode 100644 index 4d9ac87e..00000000 --- a/bci/version_control/states/revisions/firefox.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Optional - -from bci.database.mongo.revision_cache import RevisionCache -from bci.version_control.states.revisions.base import BaseRevision -from bci.version_control.states.state import State - - -class FirefoxRevision(BaseRevision): - def __init__( - self, revision_id: Optional[str] = None, revision_nb: Optional[int] = None, major_version: Optional[int] = None - ): - super().__init__(revision_id=revision_id, revision_nb=revision_nb) - self.major_version = major_version - - @property - def browser_name(self) -> str: - return 'firefox' - - def has_online_binary(self) -> bool: - return RevisionCache.firefox_has_binary_for(revision_nb=self.revision_nb, revision_id=self._revision_id) - - def get_online_binary_urls(self) -> list[str]: - result = RevisionCache.firefox_get_binary_info(self._revision_id) - if result is None: - raise AttributeError(f"Could not find binary url for '{self._revision_id}'") - binary_base_url = result['files_url'] - app_version = result['app_version'] - return [ - f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.bz2', - f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.xz' - ] - - def get_previous_and_next_state_with_binary(self) -> tuple[State, State]: - previous_revision_nb, next_revision_nb = RevisionCache.firefox_get_previous_and_next_revision_nb_with_binary( - self.revision_nb - ) - - return ( - FirefoxRevision(revision_nb=previous_revision_nb) if previous_revision_nb else None, - FirefoxRevision(revision_nb=next_revision_nb) if next_revision_nb else None, - ) - - def _fetch_missing_data(self): - if self._revision_id is None: - self._revision_id = RevisionCache.firefox_get_revision_id(self._revision_nb) - if self._revision_nb is None and self._revision_id is not None: - self._revision_nb = RevisionCache.firefox_get_revision_number(self._revision_id) diff --git a/bci/version_control/states/state.py b/bci/version_control/states/state.py deleted file mode 100644 index 8fe09fa6..00000000 --- a/bci/version_control/states/state.py +++ /dev/null @@ -1,169 +0,0 @@ -from __future__ import annotations - -from abc import abstractmethod -from dataclasses import dataclass -from enum import Enum -from typing import Optional - - -class StateCondition(Enum): - """ - The condition of a state. - """ - - # This state has been evaluated and the result is available. - COMPLETED = 0 - # The evaluation of this state has failed. - FAILED = 1 - # The evaluation of this state is in progress. - IN_PROGRESS = 2 - # The evaluation of this state has not started yet. - PENDING = 3 - # This state is not available. - UNAVAILABLE = 4 - - -@dataclass(frozen=True) -class StateResult: - requests: list[dict[str, str]] - request_vars: list[dict[str, str]] - log_vars: list[dict[str, str]] - is_dirty: bool - reproduced: bool - - def has_same_outcome(self, other: StateResult) -> bool: - """ - Returns whether this and the given other result share the same outcome. - - :returns bool: True if both state results are reproduced, not reproduced, or are both dirty. - """ - return self.is_dirty == other.is_dirty and self.reproduced == other.reproduced - - def __repr__(self) -> str: - return f'StateResult(reproduced={self.reproduced}, dirty={self.is_dirty})' - - -class State: - def __init__(self): - self.result: Optional[StateResult] = None - self.unavailable = False - self.failed_by_error = False - - @property - def condition(self) -> StateCondition: - if self.result is None: - return StateCondition.PENDING - elif self.failed_by_error: - return StateCondition.FAILED - elif self.unavailable: - return StateCondition.UNAVAILABLE - elif self.result.is_dirty: - return StateCondition.FAILED - else: - return StateCondition.COMPLETED - - def has_dirty_result(self) -> bool: - """ - Returns whether this state has a dirty result. - - :returns bool: True if this state has a result, which is dirty. - """ - return self.result is not None and self.result.is_dirty - - def has_dirty_or_no_result(self) -> bool: - """ - Returns whether this state has no result or a dirty result. - - :returns bool: True if this state has no result, or a dirty result. - """ - return self.result is None or self.result.is_dirty - - def has_same_outcome(self, other: State) -> bool: - """ - Returns whether this and the given other state share the same result outcome. - - :returns bool: True if states are both reproduced, not reproduced, or dirty. - """ - if self.result is None or other.result is None: - return False - else: - return self.result.has_same_outcome(other.result) - - @property - @abstractmethod - def name(self) -> str: - pass - - @property - @abstractmethod - def browser_name(self) -> str: - pass - - @property - @abstractmethod - def type(self) -> str: - pass - - @property - @abstractmethod - def index(self) -> int: - """ - The index of the element in the sequence. - """ - pass - - @property - @abstractmethod - def revision_nb(self) -> int: - pass - - @abstractmethod - def to_dict(self) -> dict: - pass - - @staticmethod - def from_dict(data: dict) -> State: - from bci.version_control.states.revisions.base import BaseRevision - from bci.version_control.states.versions.base import BaseVersion - - match data['type']: - case 'revision': - return BaseRevision.from_dict(data) - case 'version': - return BaseVersion.from_dict(data) - case _: - raise Exception(f'Unknown state type: {data["type"]}') - - @abstractmethod - def has_online_binary(self) -> bool: - pass - - @abstractmethod - def get_online_binary_urls(self) -> list[str]: - """ - Returns a list of URLs where the associated binary can potentially be downloaded from. - """ - pass - - def has_available_binary(self) -> bool: - if self.condition == StateCondition.UNAVAILABLE: - return False - else: - has_available_binary = self.has_online_binary() - if not has_available_binary: - self.unavailable = True - return has_available_binary - - def get_previous_and_next_state_with_binary(self) -> tuple[State, State]: - raise NotImplementedError(f'This function is not implemented for {self}') - - def __repr__(self) -> str: - return f'State(index={self.index})' - - def __eq__(self, other: object) -> bool: - if not isinstance(other, State): - return False - return self.index == other.index - - def __hash__(self) -> int: - return hash((self.index, self.browser_name)) diff --git a/bci/version_control/states/versions/chromium.py b/bci/version_control/states/versions/chromium.py deleted file mode 100644 index 7dc986e1..00000000 --- a/bci/version_control/states/versions/chromium.py +++ /dev/null @@ -1,40 +0,0 @@ -import requests - -from bci.database.mongo.mongodb import MongoDB -from bci.version_control.repository.online.chromium import get_release_revision_id, get_release_revision_number -from bci.version_control.states.revisions.chromium import ChromiumRevision -from bci.version_control.states.versions.base import BaseVersion - - -class ChromiumVersion(BaseVersion): - def __init__(self, major_version: int): - super().__init__(major_version) - - def _get_rev_nb(self) -> int: - return get_release_revision_number(self.major_version) - - def _get_rev_id(self) -> str: - return get_release_revision_id(self.major_version) - - @property - def browser_name(self): - return 'chromium' - - def has_online_binary(self): - cached_binary_available_online = MongoDB().has_binary_available_online('chromium', self) - if cached_binary_available_online is not None: - return cached_binary_available_online - url = f'https://www.googleapis.com/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F{self._revision_nb}%2Fchrome-linux.zip' - req = requests.get(url) - has_binary_online = req.status_code == 200 - MongoDB().store_binary_availability_online_cache('chromium', self, has_binary_online) - return has_binary_online - - def get_online_binary_urls(self) -> list[str]: - return [( - 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/%s%%2F%s%%2Fchrome-%s.zip?alt=media' - % ('Linux_x64', self._revision_nb, 'linux') - )] - - def convert_to_revision(self) -> ChromiumRevision: - return ChromiumRevision(revision_nb=self._revision_nb) diff --git a/bci/version_control/states/versions/firefox.py b/bci/version_control/states/versions/firefox.py deleted file mode 100644 index c48bba39..00000000 --- a/bci/version_control/states/versions/firefox.py +++ /dev/null @@ -1,30 +0,0 @@ -from bci.version_control.repository.online.firefox import get_release_revision_id, get_release_revision_number -from bci.version_control.states.revisions.firefox import FirefoxRevision -from bci.version_control.states.versions.base import BaseVersion - - -class FirefoxVersion(BaseVersion): - def __init__(self, major_version: int): - super().__init__(major_version) - - def _get_rev_nb(self) -> int: - return get_release_revision_number(self.major_version) - - def _get_rev_id(self) -> str: - return get_release_revision_id(self.major_version) - - @property - def browser_name(self) -> str: - return 'firefox' - - def has_online_binary(self) -> bool: - return True - - def get_online_binary_urls(self) -> list[str]: - return [ - f'https://ftp.mozilla.org/pub/firefox/releases/{self.major_version}.0/linux-x86_64/en-US/firefox-{self.major_version}.0.tar.bz2', - f'https://ftp.mozilla.org/pub/firefox/releases/{self.major_version}.0/linux-x86_64/en-US/firefox-{self.major_version}.0.tar.xz' - ] - - def convert_to_revision(self) -> FirefoxRevision: - return FirefoxRevision(revision_nb=self._revision_nb) diff --git a/bci/web/blueprints/api.py b/bci/web/blueprints/api.py deleted file mode 100644 index f187cbef..00000000 --- a/bci/web/blueprints/api.py +++ /dev/null @@ -1,375 +0,0 @@ -import json -import logging -import os -import threading - -from flask import Blueprint, current_app, redirect, request - -import bci.browser.support as browser_support -from bci.database.mongo.mongodb import MongoDB -import bci.evaluations.logic as application_logic -from bci.analysis.plot_factory import PlotFactory -from bci.app import sock -from bci.configuration import Global, Loggers -from bci.evaluations.logic import MissingParametersException, PlotParameters -from bci.integration_tests.evaluation_configurations import get_eval_parameters_list -from bci.main import Main -from bci.web.clients import Clients - -logger = logging.getLogger(__name__) -api = Blueprint('api', __name__, url_prefix='/api') - -THREAD = None - - -def __start_thread(func, args=None): - global THREAD - if args is None: - args = [] - if THREAD and THREAD.is_alive(): - raise AttributeError() - else: - THREAD = threading.Thread(target=func, args=args) - THREAD.start() - - -def __get_main() -> Main: - if main := current_app.config['main']: - return main - raise Exception('Main object is not instantiated') - - -@api.before_request -def check_readiness(): - try: - pass - # _ = ____get_main() - except Exception as e: - logger.critical(e) - return { - 'status': 'NOK', - 'msg': 'BugHog is not ready', - 'info': { - 'log': Loggers.get_logs() - } - } - - -@api.after_request -def add_headers(response): - if 'DEVELOPMENT' in os.environ and os.environ['DEVELOPMENT'] == '1': - response.headers['Access-Control-Allow-Origin'] = '*' - response.headers['Access-Control-Allow-Headers'] = 'Content-Type' - response.headers['Access-Control-Allow-Methods'] = '*' - return response - - -''' -Starting and stopping processses -''' - - -@api.route('/evaluation/start/', methods=['POST']) -def start_evaluation(): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No evaluation parameters found" - } - - data = request.json.copy() - try: - params = application_logic.evaluation_factory(data) - __start_thread(__get_main().run, args=[params]) - return { - 'status': 'OK' - } - except MissingParametersException: - return { - 'status': 'NOK', - 'msg': 'Could not start evaluation due to missing parameters' - } - except AttributeError: - return { - 'status': 'NOK', - 'msg': 'Evaluation thread is already running' - } - - -@api.route('/evaluation/stop/', methods=['POST']) -def stop_evaluation(): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No stop parameters found" - } - - data = request.json.copy() - forcefully = data.get('forcefully', False) - if forcefully: - __get_main().activate_stop_forcefully() - else: - __get_main().activate_stop_gracefully() - return { - 'status': 'OK' - } - - -''' -Requesting information -''' - - -@sock.route('/socket/', bp=api) -def init_websocket(ws): - logger.info('Client connected') - Clients.add_client(ws) - while True: - message = ws.receive() - if message is None: - break - try: - message = json.loads(message) - if params := message.get('new_browser', None): - Clients.associate_browser(ws, params) - if params := message.get('new_params', None): - Clients.associate_params(ws, params) - if params := message.get('select_project', None): - Clients.associate_project(ws, params) - if requested_variables := message.get('get', []): - __get_main().push_info(ws, *requested_variables) - except ValueError: - logger.warning('Ignoring invalid message from client.') - ws.send('Connected to BugHog') - - -@api.route('/browsers/', methods=['GET']) -def get_browsers(): - return { - 'status': 'OK', - 'browsers': [browser_support.get_chromium_support(), browser_support.get_firefox_support()] - } - - -@api.route('/projects/', methods=['GET']) -def get_projects(): - return { - 'status': 'OK', - 'projects': __get_main().evaluation_framework.get_projects() - } - - -@api.route('/projects/', methods=['POST']) -def create_project(): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No parameters found" - } - project_name = request.json.get('project_name') - try: - __get_main().evaluation_framework.create_empty_project(project_name) - return { - 'status': 'OK' - } - except AttributeError as e: - return { - 'status': 'NOK', - 'msg': str(e) - } - - -@api.route('/system/', methods=['GET']) -def get_system_info(): - return { - 'status': 'OK', - 'cpu_count': os.cpu_count() if os.cpu_count() else 2 - } - - -@api.route('/log/', methods=['POST']) -def log(): - # TODO: emit logs of workers in central log - return { - 'status': 'OK' - } - - -@api.route('/data/', methods=['PUT']) -def data_source(): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No data parameters found" - } - - params = request.json.copy() - plot_params = PlotParameters.from_dict(params) - if missing_params := PlotFactory.validate_params(plot_params): - return { - 'status': 'NOK', - 'msg': f'Missing plot parameters: {missing_params}' - } - return { - 'status': 'OK', - 'revision': PlotFactory.get_plot_revision_data(params), - 'version': PlotFactory.get_plot_version_data(params) - } - -@api.route('/poc//', methods=['GET']) -def get_experiments(project: str): - experiments = __get_main().evaluation_framework.get_mech_groups(project) - return { - 'status': 'OK', - 'experiments': experiments - } - - -@api.route('/poc///', methods=['GET']) -def poc(project: str, poc: str): - return { - 'status': 'OK', - 'tree': __get_main().evaluation_framework.get_poc_structure(project, poc) - } - - -@api.route('/poc////', methods=['GET', 'POST']) -def poc_file_content(project: str, poc: str, file: str): - domain = request.args.get('domain', '') - path = request.args.get('path', '') - if request.method == 'GET': - return { - 'status': 'OK', - 'content': __get_main().evaluation_framework.get_poc_file(project, poc, domain, path, file) - } - else: - if not request.json: - return { - 'status': 'NOK', - 'msg': 'No content to update file with' - } - data = request.json.copy() - content = data['content'] - success = __get_main().evaluation_framework.update_poc_file(project, poc, domain, path, file, content) - if success: - return { - 'status': 'OK' - } - else : - return { - 'status': 'NOK' - } - - -@api.route('/poc///', methods=['POST']) -def add_page(project: str, poc: str): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No page parameters found" - } - - data = request.json.copy() - domain = data['domain'] - path = data['page'] - file_type = data['file_type'] - try: - __get_main().evaluation_framework.add_page(project, poc, domain, path, file_type) - return { - 'status': 'OK' - } - except AttributeError as e: - return { - 'status': 'NOK', - 'msg': str(e) - } - - -@api.route('/poc///config', methods=['POST']) -def add_config(project: str, poc: str): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No parameters found" - } - data = request.json.copy() - type = data['type'] - success = __get_main().evaluation_framework.add_config(project, poc, type) - if success: - return { - 'status': 'OK' - } - else: - return { - 'status': 'NOK' - } - - -@api.route('/poc/domain/', methods=['GET']) -def get_available_domains(): - return { - 'status': 'OK', - 'domains': Global.get_available_domains() - } - - -@api.route('/poc//', methods=['POST']) -def create_experiment(project: str): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No experiment parameters found" - } - - data = request.json.copy() - if 'poc_name' not in data.keys(): - return { - 'status': 'NOK', - 'msg': 'Missing experiment name' - } - poc_name = data['poc_name'] - try: - __get_main().evaluation_framework.create_empty_poc(project, poc_name) - return { - 'status': 'OK' - } - except AttributeError as e: - return { - 'status': 'NOK', - 'msg': str(e) - } - - -@api.route('/data/remove/', methods=['POST']) -def remove_datapoint(): - if (params := application_logic.TestParameters.from_dict(request.json)) is None: - return { - 'status': 'NOK', - 'msg': "No parameters found" - } - __get_main().remove_datapoint(params) - return { - 'status': 'OK' - } - - -@api.route('/test/start/', methods=['POST']) -def integration_tests_start(): - # Remove all previous data - MongoDB().remove_all_data_from_collection('integrationtests_chromium') - MongoDB().remove_all_data_from_collection('integrationtests_firefox') - # Start integration tests - all_experiments = __get_main().evaluation_framework.get_mech_groups('IntegrationTests') - elegible_experiments = [experiment[0] for experiment in all_experiments if experiment[1]] - eval_parameters_list = get_eval_parameters_list(elegible_experiments) - __start_thread(__get_main().run, args=[eval_parameters_list]) - return redirect('/test/') - - -@api.route('/test/continue/', methods=['POST']) -def integration_tests_continue(): - all_experiments = __get_main().evaluation_framework.get_mech_groups('IntegrationTests') - elegible_experiments = [experiment[0] for experiment in all_experiments if experiment[1]] - eval_parameters_list = get_eval_parameters_list(elegible_experiments) - __start_thread(__get_main().run, args=[eval_parameters_list]) - return redirect('/test/') diff --git a/bci/worker.py b/bci/worker.py deleted file mode 100644 index 97276d36..00000000 --- a/bci/worker.py +++ /dev/null @@ -1,51 +0,0 @@ -import logging -import os -import sys - -from bci.configuration import Loggers -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.custom.custom_evaluation import CustomEvaluationFramework -from bci.evaluations.logic import WorkerParameters - -# This logger argument is set explicitly so when this file is ran as a script, it will still use the logger configuration -logger = logging.getLogger('bci.worker') - - -def __run_by_worker() -> None: - """ - Executes evaluation based on given parameters. - Should only be called by worker. - """ - Loggers.configure_loggers() - if len(sys.argv) < 2: - logger.info('Worker did not receive any arguments.') - os._exit(0) - args = sys.argv[1] - - logger.info('Worker started') - database_connection_params = WorkerParameters.get_database_params(args) - MongoDB().connect(database_connection_params) - - params = WorkerParameters.deserialize(args) - run(params) - logger.info('Worker finished, exiting...') - - logging.shutdown() - os._exit(0) - - -def run(params: WorkerParameters): - """ - Executes evaluation based on given parameters. - """ - evaluation_framework = CustomEvaluationFramework() - try: - evaluation_framework.evaluate(params, is_worker=True) - except Exception: - logger.fatal("An exception occurred during evaluation", exc_info=True) - logging.shutdown() - os._exit(1) - - -if __name__ == '__main__': - __run_by_worker() diff --git a/bci/__init__.py b/bughog/__init__.py similarity index 100% rename from bci/__init__.py rename to bughog/__init__.py diff --git a/bci/analysis/__init__.py b/bughog/analysis/__init__.py similarity index 100% rename from bci/analysis/__init__.py rename to bughog/analysis/__init__.py diff --git a/bughog/analysis/plot_factory.py b/bughog/analysis/plot_factory.py new file mode 100644 index 00000000..b0af16af --- /dev/null +++ b/bughog/analysis/plot_factory.py @@ -0,0 +1,60 @@ +from bughog.database.mongo.mongodb import MongoDB +from bughog.parameters import EvaluationParameters + + +class PlotFactory: + @staticmethod + def get_plot_commit_data(params: EvaluationParameters, target_variable: str = 'reproduced') -> dict: + commit_docs = MongoDB().get_documents_for_plotting(params) + return PlotFactory.__add_outcome_info(params, commit_docs, target_variable) + + @staticmethod + def get_plot_release_data(params: EvaluationParameters, target_variable: str = 'reproduced') -> dict: + release_docs = MongoDB().get_documents_for_plotting(params, releases=True) + return PlotFactory.__add_outcome_info(params, release_docs, target_variable) + + @staticmethod + def validate_params(params: EvaluationParameters) -> list[str]: + missing_parameters = [] + if not params.evaluation_range.experiment_name: + missing_parameters.append('selected experiment') + if not params.subject_configuration.subject_type: + missing_parameters.append('subject_type') + if not params.subject_configuration.subject_name: + missing_parameters.append('subject_name') + return missing_parameters + + @staticmethod + def __transform_to_bokeh_compatible(docs: list) -> dict: + new_docs = {} + for d in docs: + for key, value in d.items(): + if key not in new_docs: + new_docs[key] = [] + new_docs[key].append(value) + return new_docs + + @staticmethod + def __add_outcome_info(params: EvaluationParameters, docs: list, target_variable: str): + if not docs: + return {'revision_number': [], 'browser_version': [], 'browser_version_str': [], 'outcome': []} + docs_with_outcome = [] + + for doc in docs: + result_variables = doc['result_variables'] + new_doc = { + 'revision_number': doc['state']['commit_number'], + 'subject_version': int(doc['subject_version'].split('.')[0]), + 'subject_version_str': doc['subject_version'].split('.')[0], + } + if doc['dirty']: + new_doc['outcome'] = 'Error' + docs_with_outcome.append(new_doc) + elif {'reproduced': 'ok'} in result_variables: + new_doc['outcome'] = 'Reproduced' + docs_with_outcome.append(new_doc) + else: + new_doc['outcome'] = 'Not reproduced' + docs_with_outcome.append(new_doc) + docs_with_outcome = PlotFactory.__transform_to_bokeh_compatible(docs_with_outcome) + return docs_with_outcome diff --git a/bci/app.py b/bughog/app.py similarity index 84% rename from bci/app.py rename to bughog/app.py index 8bf06ccc..d6d3d822 100644 --- a/bci/app.py +++ b/bughog/app.py @@ -4,8 +4,8 @@ from flask import Flask from flask_sock import Sock -from bci.configuration import Global, Loggers -from bci.main import Main +from bughog.configuration import Global, Loggers +from bughog.main import Main sock = Sock() @@ -23,9 +23,9 @@ def create_app(): main = Main() # Blueprint modules are only imported after loggers are configured - from bci.web.blueprints.api import api - from bci.web.blueprints.experiments import exp - from bci.web.blueprints.test import test + from bughog.web.blueprints.api import api + from bughog.web.blueprints.experiments import exp + from bughog.web.blueprints.test import test app = Flask(__name__) # We don't store anything sensitive in the session, so we can use a simple secret key diff --git a/bci/cli.py b/bughog/cli.py similarity index 100% rename from bci/cli.py rename to bughog/cli.py diff --git a/bci/configuration.py b/bughog/configuration.py similarity index 95% rename from bci/configuration.py rename to bughog/configuration.py index 4aa4692a..7e819afe 100644 --- a/bci/configuration.py +++ b/bughog/configuration.py @@ -3,8 +3,8 @@ import os import sys -import bci.database.mongo.container as container -from bci.evaluations.logic import DatabaseParameters +import bughog.database.mongo.container as container +from bughog.parameters import DatabaseParameters logger = logging.getLogger(__name__) @@ -97,16 +97,6 @@ def get_tag() -> str: return bughog_version -class Chromium: - extension_folder = '/app/browser/extensions/chromium' - repo_to_use = 'online' - - -class Firefox: - extension_folder = '/app/browser/extensions/firefox' - repo_to_use = 'online' - - class CustomHTTPHandler(logging.handlers.HTTPHandler): def __init__( self, host: str, url: str, method: str = 'GET', secure: bool = False, credentials=None, context=None diff --git a/bci/browser/__init__.py b/bughog/database/__init__.py similarity index 100% rename from bci/browser/__init__.py rename to bughog/database/__init__.py diff --git a/bci/browser/automation/__init__.py b/bughog/database/mongo/__init__.py similarity index 100% rename from bci/browser/automation/__init__.py rename to bughog/database/mongo/__init__.py diff --git a/bughog/database/mongo/cache.py b/bughog/database/mongo/cache.py new file mode 100644 index 00000000..e30ccaf2 --- /dev/null +++ b/bughog/database/mongo/cache.py @@ -0,0 +1,33 @@ +import functools +from datetime import datetime, timezone + +from bughog.database.mongo.mongodb import MongoDB + + +class Cache: + + @staticmethod + def cache_in_db(subject_type: str, subject_name: str): + def decorator(func): + @functools.wraps(func) + def wrapper(key): + collection = MongoDB().get_cache_collection(subject_type) + doc = collection.find_one({ + 'subject_name': subject_name, + 'function_name': func.__name__, + 'key': key + }) + if doc and 'value' in doc: + return doc['value'] + + new_value = func(key) + if new_value is not None: + collection.insert_one({ + 'subject_name': subject_name, + 'key': key, + 'value': new_value, + 'ts': str(datetime.now(timezone.utc).replace(microsecond=0)), + }) + return new_value + return wrapper + return decorator diff --git a/bci/database/mongo/container.py b/bughog/database/mongo/container.py similarity index 97% rename from bci/database/mongo/container.py rename to bughog/database/mongo/container.py index dbe49dcb..c7fda920 100644 --- a/bci/database/mongo/container.py +++ b/bughog/database/mongo/container.py @@ -5,7 +5,7 @@ import docker.errors from pymongo import MongoClient -from bci.evaluations.logic import DatabaseParameters +from bughog.parameters import DatabaseParameters LOGGER = logging.getLogger(__name__) diff --git a/bci/database/mongo/binary_cache.py b/bughog/database/mongo/executable_cache.py similarity index 50% rename from bci/database/mongo/binary_cache.py rename to bughog/database/mongo/executable_cache.py index 9c832250..44371142 100644 --- a/bci/database/mongo/binary_cache.py +++ b/bughog/database/mongo/executable_cache.py @@ -5,34 +5,36 @@ import time from typing import Optional -from bci.database.mongo.mongodb import MongoDB -from bci.version_control.states.state import State +from bughog.database.mongo.mongodb import MongoDB +from bughog.parameters import SubjectConfiguration +from bughog.version_control.state.base import State logger = logging.getLogger(__name__) -class BinaryCache: +class ExecutableCache: """ - The binary cache is used to store and fetch binary files from the database. + The executable cache is used to store and fetch executable files from the database. """ @staticmethod - def fetch_binary_files(binary_executable_path: str, state: State) -> bool: + def fetch_executable_files(subject_config: SubjectConfiguration, state: State, executable_path: str) -> bool: """ - Fetches the binary files from the database and stores them in the directory of the given path. + Fetches the executable files from the database and stores them in the directory of the given path. - :param binary_executable_path: The path to store the executable binary file. - :param state: The state of the binary. - :return: True if the binary was fetched, False otherwise. + :param executable_path: The path to store the executable files. + :param state: The state of the executable. + :return: True if the executable was fetched, False otherwise. """ - if MongoDB().binary_cache_limit <= 0: + if MongoDB().executable_cache_limit <= 0: return False files_collection = MongoDB().get_collection('fs.files') query = { - 'file_type': 'binary', - 'browser_name': state.browser_name, + 'file_type': 'executable', + 'subject_type': subject_config.subject_type, + 'subject_name': subject_config.subject_name, 'state_type': state.type, 'state_index': state.index, } @@ -43,9 +45,9 @@ def fetch_binary_files(binary_executable_path: str, state: State) -> bool: query, {'$inc': {'access_count': 1}, '$set': {'last_access_ts': datetime.datetime.now()}}, ) - binary_folder_path = os.path.dirname(binary_executable_path) - if not os.path.exists(binary_folder_path): - os.mkdir(binary_folder_path) + executable_folder_path = os.path.dirname(executable_path) + if not os.path.exists(executable_folder_path): + os.mkdir(executable_folder_path) def write_from_db(file_path: str, grid_file_id: str) -> None: grid_file = fs.get(grid_file_id) @@ -59,51 +61,54 @@ def write_from_db(file_path: str, grid_file_id: str) -> None: start_time = time.time() with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: for grid_doc in grid_cursor: - file_path = os.path.join(binary_folder_path, grid_doc['relative_file_path']) + file_path = os.path.join(executable_folder_path, grid_doc['relative_file_path']) grid_file_id = grid_doc['_id'] executor.submit(write_from_db, file_path, grid_file_id) executor.shutdown(wait=True) elapsed_time = time.time() - start_time - logger.debug(f'Fetched cached binary in {elapsed_time:.2f}s') + logger.debug(f'Fetched cached executable in {elapsed_time:.2f}s') return True @staticmethod - def store_binary_files(binary_executable_path: str, state: State): + def store_executable_files(subject_config: SubjectConfiguration, state: State, executable_path: str): """ Stores the files in the folder of the given path in the database. - :param binary_executable_path: The path to the binary executable. - :param state: The state of the binary. - :return: True if the binary was stored, False otherwise. + :param subject_config: The evaluation subject configuration. + :param state: The state of the executable. + :param executable_path: The path to the executable. + :return: True if the executable was stored, False otherwise. """ - if MongoDB().binary_cache_limit <= 0: + if MongoDB().executable_cache_limit <= 0: return False - while BinaryCache.__count_cached_binaries() >= MongoDB().binary_cache_limit: - if BinaryCache.__count_cached_binaries(state_type='revision') <= 0: + while ExecutableCache.__count_cached_executables() >= MongoDB().executable_cache_limit: + if ExecutableCache.__count_cached_executables(state_type='revision') <= 0: # There are only version binaries in the cache, which will never be removed return False - BinaryCache.__remove_least_used_revision_binary_files() + ExecutableCache.__remove_least_used_revision_executable_files() - logger.debug(f"Caching binary files for {state}...") + logger.debug(f'Caching executable files for {state}...') fs = MongoDB().gridfs - binary_folder_path = os.path.dirname(binary_executable_path) + executable_folder_path = os.path.dirname(executable_path) last_access_ts = datetime.datetime.now() + def store_file(file_path: str) -> None: # Max chunk size is 16 MB (meta-data included) chunk_size = 1024 * 1024 * 15 with open(file_path, 'rb') as file: file_id = fs.new_file( - file_type='binary', - browser_name=state.browser_name, - state_type=state.type, - state_index=state.index, - relative_file_path=os.path.relpath(file_path, binary_folder_path), - access_count=0, - last_access_ts=last_access_ts, - chunk_size=chunk_size + file_type='executable', + subject_type=subject_config.subject_type, + subject_name=subject_config.subject_name, + state_type=state.type, + state_index=state.index, + relative_file_path=os.path.relpath(file_path, executable_folder_path), + access_count=0, + last_access_ts=last_access_ts, + chunk_size=chunk_size, ) while chunk := file.read(chunk_size): file_id.write(chunk) @@ -112,35 +117,35 @@ def store_file(file_path: str) -> None: start_time = time.time() with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: futures = [] - for root, _, files in os.walk(binary_folder_path): + for root, _, files in os.walk(executable_folder_path): for file in files: file_path = os.path.join(root, file) future = executor.submit(store_file, file_path) futures.append(future) - logger.debug(f"Number of files to cache: {len(futures)}") + logger.debug(f'Number of files to cache: {len(futures)}') executor.shutdown(wait=True) futures_with_exception = [future for future in futures if future.exception() is not None] if futures_with_exception: logger.error( ( - f"Something went wrong caching binary files for {state}, " - "Removing possibly imcomplete binary files from cache." + f'Something went wrong caching executable files for {state}, ' + 'Removing possibly imcomplete executable files from cache.' ), - exc_info=futures_with_exception[0].exception() + exc_info=futures_with_exception[0].exception(), ) - BinaryCache.__remove_revision_binary_files(state.type, state.index) - logger.debug(f"Removed possibly incomplete cached binary files for {state}.") + ExecutableCache.__remove_revision_executable_files(state.type, state.index) + logger.debug(f'Removed possibly incomplete cached executable files for {state}.') else: elapsed_time = time.time() - start_time - logger.debug(f'Stored binary in {elapsed_time:.2f}s') + logger.debug(f'Stored executable in {elapsed_time:.2f}s') @staticmethod - def remove_binary_files(state: State) -> None: - BinaryCache.__remove_revision_binary_files(state.type, state.index) + def remove_executable_files(state: State) -> None: + ExecutableCache.__remove_revision_executable_files(state.type, state.index) @staticmethod - def __count_cached_binaries(state_type: Optional[str] = None) -> int: + def __count_cached_executables(state_type: Optional[str] = None) -> int: """ Counts the number of cached binaries in the database. @@ -149,31 +154,31 @@ def __count_cached_binaries(state_type: Optional[str] = None) -> int: """ files_collection = MongoDB().get_collection('fs.files') if state_type: - query = {'file_type': 'binary', 'state_type': state_type} + query = {'file_type': 'executable', 'state_type': state_type} else: - query = {'file_type': 'binary'} + query = {'file_type': 'executable'} return len(files_collection.find(query).distinct('state_index')) @staticmethod - def __remove_least_used_revision_binary_files() -> None: + def __remove_least_used_revision_executable_files() -> None: """ - Removes the least used revision binary files from the database. + Removes the least used revision executable files from the database. """ files_collection = MongoDB().get_collection('fs.files') grid_cursor = files_collection.find( - {'file_type': 'binary', 'state_type': 'revision'}, + {'file_type': 'executable', 'state_type': 'revision'}, sort=[('access_count', 1), ('last_access_ts', 1)], ) for state_doc in grid_cursor: state_index = state_doc['state_index'] - BinaryCache.__remove_revision_binary_files('revision', state_index) + ExecutableCache.__remove_revision_executable_files('revision', state_index) break @staticmethod - def __remove_revision_binary_files(state_type: str, state_index: int) -> None: + def __remove_revision_executable_files(state_type: str, state_index: int) -> None: """ - Removes the binary files associated with the parameters. + Removes the executable files associated with the parameters. """ fs = MongoDB().gridfs files_collection = MongoDB().get_collection('fs.files') diff --git a/bci/database/mongo/mongodb.py b/bughog/database/mongo/mongodb.py similarity index 51% rename from bci/database/mongo/mongodb.py rename to bughog/database/mongo/mongodb.py index 57029e00..23a716fb 100644 --- a/bci/database/mongo/mongodb.py +++ b/bughog/database/mongo/mongodb.py @@ -4,22 +4,19 @@ from datetime import datetime, timezone from typing import Optional -from flatten_dict import flatten from gridfs import GridFS from pymongo import ASCENDING, MongoClient from pymongo.collection import Collection from pymongo.database import Database from pymongo.errors import ServerSelectionTimeoutError -from bci.evaluations.logic import ( +from bughog.evaluation.experiment_result import ExperimentResult +from bughog.parameters import ( DatabaseParameters, EvaluationParameters, - PlotParameters, - TestParameters, - TestResult, + SubjectConfiguration, ) -from bci.version_control.state_result_factory import StateResultFactory -from bci.version_control.states.state import State +from bughog.version_control.state.base import State logger = logging.getLogger(__name__) @@ -38,7 +35,7 @@ def get_instance(*args, **kwargs): @singleton class MongoDB: instance = None - binary_cache_limit = 0 + executable_cache_limit = 0 binary_availability_collection_names = { 'chromium': 'chromium_binary_availability', @@ -123,13 +120,13 @@ def get_collection(self, name: str, create_if_not_found: bool = False) -> Collec raise ServerException(f"Could not find collection '{name}'") return self._db[name] - def get_all_collection_names_for_browser(self, browser_name: str) -> list[str]: - """ - Returns all collections associated with the given browser. - """ + def get_cache_collection(self, subject_type: str) -> Collection: if self._db is None: raise ServerException('Database server does not have a database') - return self._db.list_collection_names(filter={'name': {'$regex': rf'^.+_{browser_name}$'}}) + collection_name = f'{subject_type}_cache' + if collection_name not in self._db.list_collection_names(): + return self._db.create_collection(collection_name) + return self._db[collection_name] @property def gridfs(self) -> GridFS: @@ -137,88 +134,94 @@ def gridfs(self) -> GridFS: raise ServerException('Database server does not have a database') return GridFS(self._db) - def store_result(self, result: TestResult): + def store_result(self, eval_params: EvaluationParameters, result: ExperimentResult): """ Upserts the result. """ - browser_config = result.params.browser_configuration - eval_config = result.params.evaluation_configuration - collection = self.__get_data_collection(result.params) + subject_config = eval_params.subject_configuration + eval_params = eval_params + collection = self.__get_data_collection(eval_params) query = { - 'browser_automation': eval_config.automation, - 'browser_version': result.browser_version, - 'binary_origin': result.binary_origin, - 'padded_browser_version': result.padded_browser_version, - 'browser_config': browser_config.browser_setting, - 'cli_options': browser_config.cli_options, - 'extensions': browser_config.extensions, - 'state': result.params.state.to_dict(), - 'mech_group': result.params.mech_group + 'subject_version': result.executable_version, + 'binary_origin': result.executable_origin, + 'padded_browser_version': result.padded_subject_version, + 'browser_config': subject_config.subject_setting, + 'cli_options': subject_config.cli_options, + 'extensions': subject_config.extensions, + 'state': result.state, + 'project': eval_params.evaluation_configuration.project, + 'experiment': eval_params.evaluation_range.experiment_name, } - if result.driver_version: - query['driver_version'] = result.driver_version - - if browser_config.browser_name == 'firefox': - build_id = self.get_build_id_firefox(result.params.state) - if build_id is None: - query['artisanal'] = True - query['build_id'] = 'artisanal' - else: - query['build_id'] = build_id + # if browser_config.subject_name == 'firefox': + # build_id = self.get_build_id_firefox(result.params.state) + # if build_id is None: + # query['artisanal'] = True + # query['build_id'] = 'artisanal' + # else: + # query['build_id'] = build_id update = { '$set': { - 'results': result.data, + 'raw_results': result.raw_results, + 'result_variables': result.result_variables, 'dirty': result.is_dirty, 'ts': str(datetime.now(timezone.utc).replace(microsecond=0)), } } collection.update_one(query, update, upsert=True) - def get_result(self, params: TestParameters) -> Optional[TestResult]: + def get_result(self, params: EvaluationParameters, state: State) -> Optional[ExperimentResult]: collection = self.__get_data_collection(params) - query = self.__to_test_query(params) + query = self.__to_experiment_query(params, state) document = collection.find_one(query) if document: - return params.create_test_result_with( - document['browser_version'], document['binary_origin'], document['results'], document['dirty'] + return ExperimentResult( + document['executable_version'], + document['executable_origin'], + document['state'], + document['raw_results'], + document['result_variables'], + document['dirty'], ) else: - logger.error(f'Could not find document for query {query}') + logger.error(f'Could not find document for query {query}.') return None - def has_result(self, params: TestParameters) -> bool: + def has_result(self, params: EvaluationParameters, state: State) -> bool: collection = self.__get_data_collection(params) - query = self.__to_test_query(params) + query = self.__to_experiment_query(params, state) nb_of_documents = collection.count_documents(query) return nb_of_documents > 0 def get_evaluated_states( - self, params: EvaluationParameters, boundary_states: Optional[tuple[State, State]], result_factory: StateResultFactory, dirty: Optional[bool]=None + self, + params: EvaluationParameters, + boundary_states: Optional[tuple[State, State]], + dirty: Optional[bool] = None, ) -> list[State]: - collection = self.get_collection(params.database_collection, create_if_not_found=True) + collection = self.__get_data_collection(params) query = { - 'browser_config': params.browser_configuration.browser_setting, - 'mech_group': params.evaluation_range.mech_group, - 'state.browser_name': params.browser_configuration.browser_name, + 'browser_config': params.subject_configuration.subject_setting, + 'experiment': params.evaluation_range.experiment_name, + 'state.browser_name': params.subject_configuration.subject_name, 'results': {'$exists': True}, 'state.type': 'version' if params.evaluation_range.only_release_revisions else 'revision', } if boundary_states is not None: query['state.revision_number'] = { - '$gte': boundary_states[0].revision_nb, - '$lte': boundary_states[1].revision_nb, + '$gte': boundary_states[0].commit_nb, + '$lte': boundary_states[1].commit_nb, } - if params.browser_configuration.extensions: + if params.subject_configuration.extensions: query['extensions'] = { - '$size': len(params.browser_configuration.extensions), - '$all': params.browser_configuration.extensions, + '$size': len(params.subject_configuration.extensions), + '$all': params.subject_configuration.extensions, } else: query['extensions'] = [] - if params.browser_configuration.cli_options: + if params.subject_configuration.cli_options: query['cli_options'] = { - '$size': len(params.browser_configuration.cli_options), - '$all': params.browser_configuration.cli_options, + '$size': len(params.subject_configuration.cli_options), + '$all': params.subject_configuration.cli_options, } else: query['cli_options'] = [] @@ -228,122 +231,131 @@ def get_evaluated_states( states = [] for doc in cursor: state = State.from_dict(doc['state']) - state.result = result_factory.get_result(doc['results']) + state.result_variables = doc['result_variables'] states.append(state) return states - def __to_test_query(self, params: TestParameters) -> dict: + def __to_experiment_query(self, params: EvaluationParameters, state: State) -> dict: query = { - 'state': params.state.to_dict(), + 'state': state.to_dict(), 'browser_automation': params.evaluation_configuration.automation, - 'browser_config': params.browser_configuration.browser_setting, - 'mech_group': params.mech_group, + 'browser_config': params.subject_configuration.subject_setting, + 'experiment': params.evaluation_range.experiment_name, } - if len(params.browser_configuration.extensions) > 0: + if len(params.subject_configuration.extensions) > 0: query['extensions'] = { - '$size': len(params.browser_configuration.extensions), - '$all': params.browser_configuration.extensions, + '$size': len(params.subject_configuration.extensions), + '$all': params.subject_configuration.extensions, } else: query['extensions'] = [] - if len(params.browser_configuration.cli_options) > 0: + if len(params.subject_configuration.cli_options) > 0: query['cli_options'] = { - '$size': len(params.browser_configuration.cli_options), - '$all': params.browser_configuration.cli_options, + '$size': len(params.subject_configuration.cli_options), + '$all': params.subject_configuration.cli_options, } else: query['cli_options'] = [] return query - def __get_data_collection(self, test_params: TestParameters) -> Collection: - collection_name = test_params.database_collection + def __get_data_collection(self, eval_params: EvaluationParameters) -> Collection: + """ + Returns the data collection, of which the name is formatted as '{subject_type}_{subject_name}'. + """ + collection_name = ( + f'{eval_params.subject_configuration.subject_type}_{eval_params.subject_configuration.subject_name}' + ) return self.get_collection(collection_name, create_if_not_found=True) - def get_binary_availability_collection(self, browser_name: str): - collection_name = self.binary_availability_collection_names[browser_name] + def get_binary_availability_collection(self, subject_config: SubjectConfiguration) -> Collection: + collection_name = f'{subject_config.subject_type}_executable_availability' return self.get_collection(collection_name, create_if_not_found=True) - # Caching of online binary availability + # Caching of online executable availability - def has_binary_available_online(self, browser: str, state: State): - collection = self.get_binary_availability_collection(browser) - document = collection.find_one({'state': state.to_dict()}) + def has_executable_available_online(self, subject_config: SubjectConfiguration, state: State): + collection = self.get_binary_availability_collection(subject_config) + document = collection.find_one({'subject_name': subject_config.subject_name, 'state': state}) if document is None: return None - return document['binary_online'] + return document['executable_online'] - def get_stored_binary_availability(self, browser): - collection = MongoDB().get_binary_availability_collection(browser) + def get_stored_binary_availability(self, subject_config: SubjectConfiguration): + collection = MongoDB().get_binary_availability_collection(subject_config) result = collection.find( - {'binary_online': True}, + {'executable_online': True}, { '_id': False, 'state': True, }, ) - if browser == 'firefox': + if subject_config.subject_name == 'firefox': result.sort('build_id', -1) return result - def get_complete_state_dict_from_binary_availability_cache(self, state: State) -> Optional[dict]: - collection = MongoDB().get_binary_availability_collection(state.browser_name) - # We have to flatten the state dictionary to ignore missing attributes. - state_dict = {'state': state.to_dict()} - query = flatten(state_dict, reducer='dot') - document = collection.find_one(query) - if document is None: - return None - return document['state'] - - def store_binary_availability_online_cache( - self, browser: str, state: State, binary_online: bool, url: Optional[str] = None - ): - collection = MongoDB().get_binary_availability_collection(browser) - collection.update_one( - {'state': state.to_dict()}, - { - '$set': { - 'state': state.to_dict(), - 'binary_online': binary_online, - 'url': url, - 'ts': str(datetime.now(timezone.utc).replace(microsecond=0)), - } - }, - upsert=True, - ) + # def get_complete_state_dict_from_binary_availability_cache(self, state: State) -> Optional[dict]: + # collection = MongoDB().get_binary_availability_collection(state.browser_name) + # # We have to flatten the state dictionary to ignore missing attributes. + # state_dict = {'state': state.to_dict()} + # query = flatten(state_dict, reducer='dot') + # document = collection.find_one(query) + # if document is None: + # return None + # return document['state'] + + # def store_executable_availability_online_cache( + # self, subject_type: str, subject_name: str, state: State, executable_online: bool, url: Optional[str] = None + # ): + # collection = MongoDB().get_binary_availability_collection(browser) + # collection.update_one( + # {'state': state.to_dict()}, + # { + # '$set': { + # 'state': state.to_dict(), + # 'executable_online': executable_online, + # 'url': url, + # 'ts': str(datetime.now(timezone.utc).replace(microsecond=0)), + # } + # }, + # upsert=True, + # ) def get_build_id_firefox(self, state: State): collection = MongoDB().get_binary_availability_collection('firefox') result = collection.find_one({'state': state.to_dict()}, {'_id': False, 'build_id': 1}) - # Result can only be None if the binary associated with the state_id is artisanal: + # Result can only be None if the executable associated with the state_id is artisanal: # This state_id will not be included in the binary_availability_collection and not have a build_id. if result is None or len(result) == 0: return None return result['build_id'] - def get_documents_for_plotting(self, params: PlotParameters, releases: bool = False) -> list: - collection = self.get_collection(params.database_collection, create_if_not_found=True) + def get_documents_for_plotting(self, params: EvaluationParameters, releases: bool = False) -> list: + collection = self.__get_data_collection(params) + + evaluation_range = params.evaluation_range + subject_config = params.subject_configuration + query = { - 'mech_group': params.mech_group, - 'browser_config': params.browser_config, + 'experiment': evaluation_range.experiment_name, + 'browser_config': subject_config.subject_name, 'state.type': 'version' if releases else 'revision', - 'extensions': {'$size': len(params.extensions) if params.extensions else 0}, - 'cli_options': {'$size': len(params.cli_options) if params.cli_options else 0}, + 'extensions': {'$size': len(subject_config.extensions) if subject_config.extensions else 0}, + 'cli_options': {'$size': len(subject_config.cli_options) if subject_config.cli_options else 0}, } - if params.extensions: - query['extensions']['$all'] = params.extensions - if params.cli_options: - query['cli_options']['$all'] = params.cli_options - if params.revision_number_range: + if subject_config.extensions: + query['extensions']['$all'] = subject_config.extensions + if subject_config.cli_options: + query['cli_options']['$all'] = subject_config.cli_options + if evaluation_range.revision_number_range: query['state.revision_number'] = { - '$gte': params.revision_number_range[0], - '$lte': params.revision_number_range[1], + '$gte': evaluation_range.revision_number_range[0], + '$lte': evaluation_range.revision_number_range[1], } - elif params.major_version_range: + elif evaluation_range.major_version_range: query['padded_browser_version'] = { - '$gte': str(params.major_version_range[0]).zfill(4), - '$lte': str(params.major_version_range[1] + 1).zfill(4), + '$gte': str(evaluation_range.major_version_range[0]).zfill(4), + '$lte': str(evaluation_range.major_version_range[1] + 1).zfill(4), } docs = collection.aggregate( @@ -355,9 +367,9 @@ def get_documents_for_plotting(self, params: PlotParameters, releases: bool = Fa ) return list(docs) - def remove_datapoint(self, params: TestParameters) -> None: - collection = self.get_collection(params.database_collection) - query = self.__to_test_query(params) + def remove_datapoint(self, params: EvaluationParameters, state: State) -> None: + collection = self.__get_data_collection(params) + query = self.__to_experiment_query(params, state) collection.delete_one(query) def remove_all_data_from_collection(self, collection_name: str) -> None: @@ -374,24 +386,17 @@ def get_previous_cli_options(self, params: dict) -> list[str]: """ Returns a list of all cli options used for the browser defined in the given parameter dictionary. """ - if browser_name := params.get('browser_name', None): - collection_names = self.get_all_collection_names_for_browser(browser_name) - previous_cli_options = [] - for name in collection_names: - # Appartently simply asking for a set of distinct arrays requires a complicated pipeline in MongoDB, - # so we'll use Python logic. - cursor = self.get_collection(name).find( - {'cli_options': {'$exists': True, '$not': {'$size': 0}}}, {'_id': False, 'cli_options': True} - ) - # We convert to tuples because they are, in contract to lists, hashable. - cli_options_list = set(' '.join(doc['cli_options']) for doc in cursor) - if cli_options_list: - previous_cli_options.extend(list(filter(lambda x: x not in previous_cli_options, cli_options_list))) - previous_cli_options.sort() - return previous_cli_options - else: - logger.warning('Could not find browser name in parameters, returning empty list') - return [] + previous_cli_options = [] + collection = self.__get_data_collection(params) + cursor = collection.find( + {'cli_options': {'$exists': True, '$not': {'$size': 0}}}, {'_id': False, 'cli_options': True} + ) + # We convert to tuples because they are, in contrast to lists, hashable. + cli_options_list = set(' '.join(doc['cli_options']) for doc in cursor) + if cli_options_list: + previous_cli_options.extend(list(filter(lambda x: x not in previous_cli_options, cli_options_list))) + previous_cli_options.sort() + return previous_cli_options class ServerException(Exception): diff --git a/bci/distribution/worker_manager.py b/bughog/distribution/worker_manager.py similarity index 75% rename from bci/distribution/worker_manager.py rename to bughog/distribution/worker_manager.py index 3390a1cc..8190435f 100644 --- a/bci/distribution/worker_manager.py +++ b/bughog/distribution/worker_manager.py @@ -7,10 +7,11 @@ import docker import docker.errors -from bci import worker -from bci.configuration import Global -from bci.evaluations.logic import WorkerParameters -from bci.web.clients import Clients +from bughog import worker +from bughog.configuration import Global +from bughog.parameters import EvaluationParameters +from bughog.version_control.state.base import State +from bughog.web.clients import Clients logger = logging.getLogger(__name__) @@ -27,15 +28,15 @@ def __init__(self, max_nb_of_containers: int) -> None: self.container_id_pool.put(i) self.client = docker.from_env() - def start_test(self, params: WorkerParameters, blocking_wait=True) -> None: + def start_experiment(self, params: EvaluationParameters, state: State, blocking_wait=True) -> None: if self.max_nb_of_containers != 1: - return self.__run_container(params, blocking_wait) + return self.__run_container(params, state, blocking_wait) # Single container mode - worker.run(params) + worker.run(params, state) Clients.push_results_to_all() - def __run_container(self, params: WorkerParameters, blocking_wait=True) -> None: + def __run_container(self, params: EvaluationParameters, state: State, blocking_wait=True) -> None: while blocking_wait and self.get_nb_of_running_worker_containers() >= self.max_nb_of_containers: time.sleep(5) container_id = self.container_id_pool.get() @@ -60,10 +61,10 @@ def start_container_thread(): break # Remove all containers with same name (never higher than 1 in practice) for container in active_containers: - logger.info(f'Removing old container \'{container.attrs["Name"]}\' to start new one') + logger.info(f"Removing old container '{container.attrs['Name']}' to start new one") container.remove(force=True) except docker.errors.APIError: - logger.error("Could not consult list of active containers", exc_info=True) + logger.error('Could not consult list of active containers', exc_info=True) container = None try: @@ -77,37 +78,32 @@ def start_container_thread(): mem_reservation='2g', detach=True, labels=['bh_worker'], - command=[params.serialize()], + command=[params.serialize(), state.serialize()], volumes=[ os.path.join(host_pwd, 'config') + ':/app/config:ro', - os.path.join(host_pwd, 'browser/binaries/chromium/artisanal') - + ':/app/browser/binaries/chromium/artisanal:rw', - os.path.join(host_pwd, 'browser/binaries/firefox/artisanal') - + ':/app/browser/binaries/firefox/artisanal:rw', - os.path.join(host_pwd, 'experiments') + ':/app/experiments:ro', - os.path.join(host_pwd, 'browser/extensions') + ':/app/browser/extensions:ro', + os.path.join(host_pwd, 'subject') + ':/app/subject:rw', os.path.join(host_pwd, 'logs') + ':/app/logs:rw', os.path.join(host_pwd, 'nginx/ssl') + ':/etc/nginx/ssl:ro', '/dev/shm:/dev/shm', ], ) result = container.wait() - if result["StatusCode"] != 0: + if result['StatusCode'] != 0: logger.error( f"'{container_name}' exited unexpectedly with status code {result['StatusCode']}. " - "Check the worker logs in ./logs/ for more information." + 'Check the worker logs in ./logs/ for more information.' ) else: - logger.debug(f"Container '{container_name}' finished experiments for '{params.state}'") + logger.debug(f"Container '{container_name}' finished experiments for '{state}'") Clients.push_results_to_all() except docker.errors.APIError: - logger.error("Received a docker error", exc_info=True) + logger.error('Received a docker error', exc_info=True) except docker.errors.ContainerError: logger.error( f"Could not run container '{container_name}' or container was unexpectedly removed", exc_info=True ) if container is not None: - container_info = container.attrs["State"] + container_info = container.attrs['State'] logger.error(f"'{container_name}' exited unexpectedly with {container_info}", exc_info=True) finally: if container is not None: @@ -116,8 +112,8 @@ def start_container_thread(): thread = threading.Thread(target=start_container_thread) thread.start() - logger.info(f"Container '{container_name}' started experiments for '{params.state}'") - # Sleep to avoid all workers downloading browser binaries at once, clogging up all IO. + logger.info(f"Container '{container_name}' started experiments for '{state}'") + # Sleep to avoid all workers downloading executables at once, clogging up all IO. time.sleep(5) def get_nb_of_running_worker_containers(self): diff --git a/bci/browser/binary/__init__.py b/bughog/evaluation/__init__.py similarity index 100% rename from bci/browser/binary/__init__.py rename to bughog/evaluation/__init__.py diff --git a/bci/evaluations/collectors/base.py b/bughog/evaluation/collectors/base.py similarity index 59% rename from bci/evaluations/collectors/base.py rename to bughog/evaluation/collectors/base.py index b6368f3c..56d6a281 100644 --- a/bci/evaluations/collectors/base.py +++ b/bughog/evaluation/collectors/base.py @@ -16,3 +16,13 @@ def stop(self): @abstractmethod def parse_data(self): pass + + @property + @abstractmethod + def raw_data(self) -> dict[str,list]: + pass + + @property + @abstractmethod + def result_variables(self) -> dict[str,str]: + pass diff --git a/bughog/evaluation/collectors/collector.py b/bughog/evaluation/collectors/collector.py new file mode 100644 index 00000000..30356279 --- /dev/null +++ b/bughog/evaluation/collectors/collector.py @@ -0,0 +1,34 @@ +import logging +from enum import Enum + +from bughog.evaluation.collectors.base import BaseCollector + +logger = logging.getLogger(__name__) + + +class Type(Enum): + REQUESTS = 1 + LOGS = 2 + + +class Collector: + def __init__(self, subcollectors: list[BaseCollector]) -> None: + self.subcollectors = subcollectors + logger.debug(f'Using {len(self.subcollectors)} result collectors') + + def start(self): + for collector in self.subcollectors: + collector.start() + + def stop(self): + for collector in self.subcollectors: + collector.stop() + + def collect_results(self) -> tuple[dict,dict[str,str]]: + raw_results = {} + result_variables = {} + for collector in self.subcollectors: + collector.parse_data() + raw_results.update(collector.data) + result_variables.update(collector.result_variables) + return raw_results, result_variables diff --git a/bci/evaluations/collectors/logs.py b/bughog/evaluation/collectors/logs.py similarity index 74% rename from bci/evaluations/collectors/logs.py rename to bughog/evaluation/collectors/logs.py index e163ded6..65a1a5ef 100644 --- a/bci/evaluations/collectors/logs.py +++ b/bughog/evaluation/collectors/logs.py @@ -7,7 +7,7 @@ class LogCollector(BaseCollector): def __init__(self) -> None: super().__init__() - self.data['log_vars'] = [] + self.data['log_vars'] = set() def start(self): with open('/tmp/browser.log', 'w') as file: @@ -17,7 +17,7 @@ def stop(self): pass def parse_data(self): - data = [] + data = set() regex = r'\+\+\+bughog_(.+)=(.+)\+\+\+' with open('/tmp/browser.log', 'r+') as log_file: log_lines = [line for line in log_file.readlines()] @@ -28,5 +28,13 @@ def parse_data(self): for match in regex_matches: var = match[0] val = match[1] - data.append({'var': var, 'val': val}) + data.add({'var': var, 'val': val}) self.data['log_vars'] = data + + @property + def raw_data(self) -> dict[str,list]: + return {'logs': []} + + @property + def result_variables(self) -> dict[str, str]: + return self.data['log_vars'] diff --git a/bci/evaluations/collectors/requests.py b/bughog/evaluation/collectors/requests.py similarity index 90% rename from bci/evaluations/collectors/requests.py rename to bughog/evaluation/collectors/requests.py index 5566340a..45968615 100644 --- a/bci/evaluations/collectors/requests.py +++ b/bughog/evaluation/collectors/requests.py @@ -62,7 +62,7 @@ def __init__(self): self.__httpd = None self.__thread = None self.data['requests'] = [] - self.data['req_vars'] = [] + self.data['req_vars'] = set() def start(self): logger.debug('Starting collector...') @@ -94,4 +94,12 @@ def parse_data(self): for key, values in parsed_query.items() if key.startswith('bughog_') ) - self.data['req_vars'] = [{'var': pair[0], 'val': pair[1]} for pair in request_variables] + self.data['req_vars'] = set({'var': pair[0], 'val': pair[1]} for pair in request_variables) + + @property + def raw_data(self) -> dict[str,list]: + return {'requests': self.data['requests']} + + @property + def result_variables(self) -> dict[str, str]: + return self.data['req_vars'] diff --git a/bughog/evaluation/custom/custom_evaluation.py b/bughog/evaluation/custom/custom_evaluation.py new file mode 100644 index 00000000..6d9442e1 --- /dev/null +++ b/bughog/evaluation/custom/custom_evaluation.py @@ -0,0 +1,220 @@ +# import logging +# import os + +# from bughog.configuration import Global +# from bughog.evaluation.collectors.collector import Collector, Type +# from bughog.evaluation.evaluation_framework import EvaluationFramework, FailedSanityCheck +# from bughog.evaluation.file_structure import Folder, parse_folder +# from bughog.parameters import ExperimentParameters, ExperimentResult +# from bughog.subject.executable import Executable +# from bughog.subject.webbrowser.interaction.interaction import BrowserInteraction +# from bughog.version_control.state_result_factory import StateResultFactory +# from bughog.web.clients import Clients + +# logger = logging.getLogger(__name__) + + +# class CustomEvaluationFramework(EvaluationFramework): +# def __init__(self): +# super().__init__() +# self.root_folder = parse_folder(Global.custom_page_folder) +# self.initialize_experiments() + +# def initialize_experiments(self): +# for project in self.root_folder.subfolders: +# project.tags.append('project') + +# for experiment in project.subfolders: +# experiment.tags.append('experiment') +# if 'script.cmd' in [file.name for file in experiment.files]: +# experiment.tags.append('runnable') + +# def __get_project_folder(self, project_name: str) -> Folder: +# for project in self.root_folder.subfolders: +# if project.name == project_name: +# return project +# raise Exception(f"Could not find project '{project_name}'") + +# def __get_experiment_folder(self, project_name: str, experiment_name: str) -> Folder: +# project = self.__get_project_folder(project_name) +# for experiment in project.subfolders: +# if experiment.name == experiment_name: +# return experiment +# raise Exception(f"Could not find experiment '{experiment_name}'") + +# def __get_interaction_script(self, project_name: str, experiment_name: str) -> list[str]: +# experiment = self.__get_experiment_folder(project_name, experiment_name) +# script_path = os.path.join(experiment.path, experiment.name, 'script.cmd') +# if os.path.isfile(script_path): +# # If an interaction script is specified, it is parsed and used +# with open(script_path) as file: +# return file.readlines() +# else: +# raise Exception(f"Could not find experiment script at '{script_path}'") + +# # @staticmethod +# # def is_runnable_experiment(project: str, poc: str, dir_tree: dict[str, dict], data: dict[str, str]) -> bool: +# # # Always runnable if there is either an interaction script or url_queue present +# # if 'script' in data or 'url_queue' in data: +# # return True + +# # # Should have exactly one main folder otherwise +# # domains = dir_tree[project][poc] +# # main_paths = [paths for paths in domains.values() if paths is not None and 'main' in paths.keys()] +# # if len(main_paths) != 1: +# # return False +# # # Main should have index.html +# # if 'index.html' not in main_paths[0]['main'].keys(): +# # return False +# # return True + +# def perform_specific_evaluation(self, executable: Executable, params: ExperimentParameters) -> ExperimentResult: +# logger.info(f'Starting test for {params}') + +# state_result_factory = StateResultFactory(experiment=params.experiment) +# collector = Collector([Type.REQUESTS, Type.LOGS]) +# collector.start() + +# is_dirty = False +# tries_left = 3 +# script = self.__get_interaction_script(params.evaluation_configuration.project, params.experiment) +# try: +# sanity_check_was_successful = False +# poc_was_reproduced = False +# while not poc_was_reproduced and tries_left > 0: +# tries_left -= 1 +# executable.pre_try_setup() +# BrowserInteraction(executable, script, params).execute() +# executable.post_try_cleanup() +# intermediary_state_result = state_result_factory.get_result(collector.collect_results()) +# sanity_check_was_successful |= not intermediary_state_result.is_dirty +# poc_was_reproduced = intermediary_state_result.reproduced +# if not poc_was_reproduced and not sanity_check_was_successful: +# raise FailedSanityCheck() +# except FailedSanityCheck: +# logger.error('Evaluation sanity check has failed', exc_info=True) +# is_dirty = True +# except Exception as e: +# logger.error(f'An error during evaluation: {e}', exc_info=True) +# is_dirty = True +# finally: +# logger.debug(f'Evaluation finished with {tries_left} tries left') +# collector.stop() +# results = collector.collect_results() +# return params.create_test_result_with(executable.version, executable.origin, results, is_dirty) + +# def get_experiments(self, project: str) -> list[tuple[str, bool]]: +# if project not in self.tests_per_project: +# return [] +# pocs = [(poc_name, poc_data['runnable']) for poc_name, poc_data in self.tests_per_project[project].items()] +# return sorted(pocs, key=lambda x: x[0]) + +# def get_projects(self) -> list[str]: +# return sorted(list(self.tests_per_project.keys())) + +# def create_empty_project(self, project_name: str): +# self.is_valid_name(project_name) +# if project_name in self.dir_tree: +# raise AttributeError(f"The given project name '{project_name}' already exists.") + +# new_project_path = os.path.join(Global.custom_page_folder, project_name) +# os.mkdir(new_project_path) +# self.reload_experiments() + +# def get_poc_structure(self, project: str, poc: str) -> dict: +# return self.dir_tree[project][poc] + +# def _get_poc_file_path(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str: +# # Top-level config file +# if domain == 'Config' and path == '_': +# return os.path.join(Global.custom_page_folder, project, poc, file_name) + +# return os.path.join(Global.custom_page_folder, project, poc, domain, path, file_name) + +# def get_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str: +# file_path = self._get_poc_file_path(project, poc, domain, path, file_name) +# if os.path.isfile(file_path): +# with open(file_path) as file: +# return file.read() +# raise AttributeError(f"Could not find PoC file at expected path '{file_path}'") + +# def update_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str, content: str) -> bool: +# file_path = self._get_poc_file_path(project, poc, domain, path, file_name) +# if os.path.isfile(file_path): +# if content == '': +# logger.warning('Attempt to save empty file ignored') +# return False +# with open(file_path, 'w') as file: +# file.write(content) +# return True +# return False + +# def create_empty_poc(self, project: str, poc_name: str): +# self.is_valid_name(poc_name) +# poc_path = os.path.join(Global.custom_page_folder, project, poc_name) +# if os.path.exists(poc_path): +# raise AttributeError(f"The given PoC name '{poc_name}' already exists.") + +# os.makedirs(poc_path) +# self.reload_experiments() +# Clients.push_experiments_to_all() + +# def add_page(self, project: str, poc: str, domain: str, path: str, file_type: str): +# domain_path = os.path.join(Global.custom_page_folder, project, poc, domain) +# if not os.path.exists(domain_path): +# os.makedirs(domain_path) + +# self.is_valid_name(path) +# if file_type == 'py': +# file_name = path if path.endswith('.py') else path + '.py' +# file_path = os.path.join(domain_path, file_name) +# else: +# page_path = os.path.join(domain_path, path) +# if not os.path.exists(page_path): +# os.makedirs(page_path) +# new_file_name = f'index.{file_type}' +# file_path = os.path.join(page_path, new_file_name) +# headers_file_path = os.path.join(page_path, 'headers.json') +# if not os.path.exists(headers_file_path): +# with open(headers_file_path, 'w') as file: +# file.write(self.get_default_file_content('headers.json')) + +# if os.path.exists(file_path): +# raise AttributeError(f"The given page '{path}' does already exist.") +# with open(file_path, 'w') as file: +# file.write(self.get_default_file_content(file_type)) + +# self.reload_experiments() +# # Notify clients of change (an experiment might now be runnable) +# Clients.push_experiments_to_all() + +# def add_config(self, project: str, poc: str, type: str) -> bool: +# content = self.get_default_file_content(type) + +# if content == '': +# return False + +# file_path = os.path.join(Global.custom_page_folder, project, poc, type) +# with open(file_path, 'w') as file: +# file.write(content) + +# self.reload_experiments() +# # Notify clients of change (an experiment might now be runnable) +# Clients.push_experiments_to_all() + +# return True + +# @staticmethod +# def get_default_file_content(file_type: str) -> str: +# path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'default_files/{file_type}') + +# if not os.path.exists(path): +# return '' + +# with open(path, 'r') as file: +# return file.read() + +# def reload_experiments(self): +# self.root_folder = parse_folder(Global.custom_page_folder) +# self.initialize_experiments() +# logger.info('Experiments are reloaded.') diff --git a/bughog/evaluation/evaluation.py b/bughog/evaluation/evaluation.py new file mode 100644 index 00000000..531c0711 --- /dev/null +++ b/bughog/evaluation/evaluation.py @@ -0,0 +1,174 @@ +import logging +import os +import re + +from bughog.configuration import Global +from bughog.database.mongo.mongodb import MongoDB +from bughog.evaluation.experiment_result import ExperimentResult +from bughog.parameters import EvaluationParameters +from bughog.subject import factory +from bughog.subject.executable import Executable, ExecutableStatus +from bughog.version_control.state.base import State +from bughog.web.clients import Clients + +logger = logging.getLogger(__name__) + + +class Evaluation: + def __init__(self, subject_type: str): + self.subject_type = subject_type + self.framework = factory.create_evaluation_framework(subject_type) + self.should_stop = False + + def conduct_experiment(self, executable: Executable, params: EvaluationParameters) -> ExperimentResult: + logger.info(f'Starting test for {params}') + + state_result_factory = StateResultFactory(experiment=params.experiment) + collector = self.framework.collector + collector.start() + + is_dirty = False + tries_left = 3 + script = self.__get_interaction_script(params.evaluation_configuration.project, params.experiment) + try: + sanity_check_was_successful = False + poc_was_reproduced = False + while not poc_was_reproduced and tries_left > 0: + tries_left -= 1 + executable.pre_try_setup() + # TODO: fix interaction + # BrowserInteraction(executable, script, params).execute() + executable.post_try_cleanup() + intermediary_result = state_result_factory.get_result(collector.collect_results()) + sanity_check_was_successful |= not intermediary_state_result.is_dirty + poc_was_reproduced = intermediary_state_result.reproduced + if not poc_was_reproduced and not sanity_check_was_successful: + raise FailedSanityCheck() + except FailedSanityCheck: + logger.error('Evaluation sanity check has failed', exc_info=True) + is_dirty = True + except Exception as e: + logger.error(f'An error during evaluation: {e}', exc_info=True) + is_dirty = True + finally: + logger.debug(f'Evaluation finished with {tries_left} tries left') + collector.stop() + raw_results, result_variables = collector.collect_results() + return ExperimentResult(executable.version, executable.origin, executable.state, raw_results, result_variables, is_dirty) + + def create_empty_project(self, project_name: str): + # TODO: call framework + self.reload_experiments() + + def update_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str, content: str) -> bool: + file_path = self._get_poc_file_path(project, poc, domain, path, file_name) + if os.path.isfile(file_path): + if content == '': + logger.warning('Attempt to save empty file ignored') + return False + with open(file_path, 'w') as file: + file.write(content) + return True + return False + + def create_empty_poc(self, project: str, poc_name: str): + self.is_valid_name(poc_name) + poc_path = os.path.join(Global.custom_page_folder, project, poc_name) + if os.path.exists(poc_path): + raise AttributeError(f"The given PoC name '{poc_name}' already exists.") + + os.makedirs(poc_path) + self.reload_experiments() + Clients.push_experiments_to_all() + + def add_page(self, project: str, poc: str, domain: str, path: str, file_type: str): + domain_path = os.path.join(Global.custom_page_folder, project, poc, domain) + if not os.path.exists(domain_path): + os.makedirs(domain_path) + + self.is_valid_name(path) + if file_type == 'py': + file_name = path if path.endswith('.py') else path + '.py' + file_path = os.path.join(domain_path, file_name) + else: + page_path = os.path.join(domain_path, path) + if not os.path.exists(page_path): + os.makedirs(page_path) + new_file_name = f'index.{file_type}' + file_path = os.path.join(page_path, new_file_name) + headers_file_path = os.path.join(page_path, 'headers.json') + if not os.path.exists(headers_file_path): + with open(headers_file_path, 'w') as file: + file.write(self.get_default_file_content('headers.json')) + + if os.path.exists(file_path): + raise AttributeError(f"The given page '{path}' does already exist.") + with open(file_path, 'w') as file: + file.write(self.get_default_file_content(file_type)) + + self.reload_experiments() + # Notify clients of change (an experiment might now be runnable) + Clients.push_experiments_to_all() + + def add_config(self, project: str, poc: str, type: str) -> bool: + content = self.get_default_file_content(type) + + if content == '': + return False + + file_path = os.path.join(Global.custom_page_folder, project, poc, type) + with open(file_path, 'w') as file: + file.write(content) + + self.reload_experiments() + # Notify clients of change (an experiment might now be runnable) + Clients.push_experiments_to_all() + + return True + + @staticmethod + def get_default_file_content(file_type: str) -> str: + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'default_files/{file_type}') + + if not os.path.exists(path): + return '' + + with open(path, 'r') as file: + return file.read() + + def evaluate(self, params: EvaluationParameters, state: State, is_worker=False): + if MongoDB().has_result(params): + logger.warning( + f"Experiment '{params.evaluation_range.experiment_name}' for '{state}' was already performed, skipping." + ) + return + + executable = create_executable(params, state) + executable.pre_evaluation_setup() + + if self.should_stop: + self.should_stop = False + return + try: + executable.pre_experiment_setup() + result = self.conduct_experiment(executable, params) + MongoDB().store_result(result) + logger.info(f'Experiment finalized: {params}') + except Exception as e: + executable.status = ExecutableStatus.EXPERIMENT_FAILED + if is_worker: + raise e + else: + logger.error('An error occurred during evaluation', exc_info=True) + finally: + executable.post_experiment_cleanup() + + executable.post_evaluation_cleanup() + logger.debug('Evaluation finished') + + def stop_gracefully(self): + self.should_stop = True + + +class FailedSanityCheck(Exception): + pass diff --git a/bughog/evaluation/experiment.py b/bughog/evaluation/experiment.py new file mode 100644 index 00000000..250af7e8 --- /dev/null +++ b/bughog/evaluation/experiment.py @@ -0,0 +1,277 @@ +import json +import logging +import os +from dataclasses import dataclass +import re +from typing import Optional + +from bughog.evaluation.file_structure import Folder +from bughog.subject.evaluation_framework import EvaluationFramework +from bughog.web.clients import Clients + +logger = logging.getLogger(__name__) + +SUPPORTED_FILE_TYPES = [ + 'css', + 'html', + 'js', + 'py', + 'xml', +] +SUPPORTED_DOMAINS = [ + 'leak.test', + 'a.test', + 'sub.a.test', + 'sub.sub.a.test', + 'b.test', + 'sub.b.test', + 'adition.com', +] +EXPERIMENT_FOLDER_PATH = '/app/experiments/pages' + + +class Experiments: + def __init__(self, subject_type: str, evaluation_framework: EvaluationFramework) -> None: + self.subject_type = subject_type + self.framework = evaluation_framework + self.root_folder = self.initialize_experiments() + + @property + def root_folder_path(self) -> str: + return f'/app/subject/{self.subject_type}/experiments/' + + def initialize_experiments(self) -> Folder: + root_folder = Folder.parse(self.root_folder_path) + for project in root_folder.subfolders: + project.tags.append('project') + for experiment in project.subfolders: + experiment.tags.append('experiment') + if 'script.cmd' in [file.name for file in experiment.files]: + experiment.tags.append('runnable') + return root_folder + + def get_projects(self) -> list[str]: + project_folders = self.root_folder.get_all_folders_with_tag('project') + return [folder.name for folder in project_folders] + + def create_empty_project(self, project_name: str): + self.__is_valid_name(project_name) + if project_name in [folder.name for folder in self.root_folder.get_all_folders_with_tag('project')]: + raise AttributeError(f"The given project name '{project_name}' already exists.") + new_project_path = os.path.join(self.root_folder_path, project_name) + os.mkdir(new_project_path) + self.reload_experiments() + + def get_experiments(self, project_name: str) -> list[tuple[str, bool]]: + project_root_folder = self.__get_project_folder(project_name) + experiment_folders = project_root_folder.get_all_folders_with_tag('experiment') + experiments = [(folder.name, 'runnable' in folder.tags) for folder in experiment_folders] + return sorted(experiments, key=lambda x: x[0]) + + def get_poc_structure(self, project, poc) -> dict: + # TODO + pass + + def _get_poc_file_path(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str: + # Top-level config file + if domain == 'Config' and path == '_': + return os.path.join(Global.custom_page_folder, project, poc, file_name) + + return os.path.join(Global.custom_page_folder, project, poc, domain, path, file_name) + + def get_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str: + file_path = self._get_poc_file_path(project, poc, domain, path, file_name) + if os.path.isfile(file_path): + with open(file_path) as file: + return file.read() + raise AttributeError(f"Could not find PoC file at expected path '{file_path}'") + + def update_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str, content: str) -> bool: + file_path = self._get_poc_file_path(project, poc, domain, path, file_name) + if os.path.isfile(file_path): + if content == '': + logger.warning('Attempt to save empty file ignored') + return False + with open(file_path, 'w') as file: + file.write(content) + return True + return False + + def add_page(self, project: str, poc: str, domain: str, path: str, file_type: str): + domain_path = os.path.join(self.root_folder_path, project, poc, domain) + if not os.path.exists(domain_path): + os.makedirs(domain_path) + + self.__is_valid_name(path) + if file_type == 'py': + file_name = path if path.endswith('.py') else path + '.py' + file_path = os.path.join(domain_path, file_name) + else: + page_path = os.path.join(domain_path, path) + if not os.path.exists(page_path): + os.makedirs(page_path) + new_file_name = f'index.{file_type}' + file_path = os.path.join(page_path, new_file_name) + headers_file_path = os.path.join(page_path, 'headers.json') + if not os.path.exists(headers_file_path): + with open(headers_file_path, 'w') as file: + file.write(self.get_default_file_content('headers.json')) + + if os.path.exists(file_path): + raise AttributeError(f"The given page '{path}' does already exist.") + with open(file_path, 'w') as file: + file.write(self.get_default_file_content(file_type)) + + self.reload_experiments() + # Notify clients of change (an experiment might now be runnable) + Clients.push_experiments_to_all() + + def add_config(self, project: str, poc: str, type: str) -> bool: + content = self.get_default_file_content(type) + + if content == '': + return False + + file_path = os.path.join(self.root_folder_path, project, poc, type) + with open(file_path, 'w') as file: + file.write(content) + + self.reload_experiments() + # Notify clients of change (an experiment might now be runnable) + Clients.push_experiments_to_all() + + return True + + def __get_project_folder(self, project_name: str) -> Folder: + for project in self.root_folder.subfolders: + if project.name == project_name: + return project + raise Exception(f"Could not find project '{project_name}'") + + def __get_experiment_folder(self, project_name: str, experiment_name: str) -> Folder: + project = self.__get_project_folder(project_name) + for experiment in project.subfolders: + if experiment.name == experiment_name: + return experiment + raise Exception(f"Could not find experiment '{experiment_name}'") + + def __get_interaction_script(self, project_name: str, experiment_name: str) -> list[str]: + experiment = self.__get_experiment_folder(project_name, experiment_name) + script_path = os.path.join(experiment.path, experiment.name, 'script.cmd') + if os.path.isfile(script_path): + # If an interaction script is specified, it is parsed and used + with open(script_path) as file: + return file.readlines() + else: + raise Exception(f"Could not find experiment script at '{script_path}'") + + def experiment_is_valid(self, project: str, experiment: str) -> bool: + return self.framework.experiment_is_valid(project, experiment) + + def create_empty_poc(self, project: str, experiment: str): + return self.framework.create_empty_experiment(project, experiment) + + def execute_script_command(self, command: str): + return self.framework.execute_script_command(command) + + def get_default_file_content(self, file_type: str) -> str: + return self.framework.get_default_file_content(file_type) + + def reload_experiments(self): + self.initialize_experiments() + logger.info('Experiments are reloaded.') + + @staticmethod + def __is_valid_name(name: str) -> None: + """ + Checks whether the given string is a valid experiment, page or project name, and raises an exception if not. + This is to prevent issues with URL encoding and decoding. + + :param name: Name to be checked on validity. + """ + if name is None or name == '': + raise AttributeError('The given name cannot be empty.') + if re.match(r'^[A-Za-z0-9_\-.]+$', name) is None: + raise AttributeError( + f"The given name '{name}' is invalid. Only letters, numbers, " + "'.', '-' and '_' can be used, and the name should not be empty." + ) + + +def verify() -> None: + """ + Verifies the experiment pages, logger warnings for unsupported configurations. + """ + for project in os.listdir(EXPERIMENT_FOLDER_PATH): + project_path = os.path.join(EXPERIMENT_FOLDER_PATH, project) + if not os.path.isdir(project_path): + logger.warning(f"Unexpected file in '{__user_path(project_path)}' will be ignored.") + continue + for experiment in os.listdir(project_path): + __verify_experiment(project, experiment) + + +def __verify_experiment(project: str, experiment: str) -> None: + experiment_path = os.path.join(EXPERIMENT_FOLDER_PATH, project, experiment) + if not os.path.isdir(experiment_path): + logger.warning(f"Unexpected file at '{__user_path(experiment_path)}' will be ignored.") + return + for domain in os.listdir(experiment_path): + if domain in ['script.cmd', 'url_queue.txt']: + continue + domain_path = os.path.join(experiment_path, domain) + if not os.path.isdir(domain_path): + logger.warning(f"Unexpected file '{__user_path(domain_path)}' will be ignored.") + continue + if domain not in SUPPORTED_DOMAINS: + logger.warning(f"Unsupported domain '{domain}' in '{__user_path(experiment_path)}' will be ignored.") + for page in os.listdir(domain_path): + __verify_page(project, experiment, domain, page) + + +def __verify_page(project: str, experiment: str, domain: str, page: str) -> None: + page_path = os.path.join(EXPERIMENT_FOLDER_PATH, project, experiment, domain, page) + if page.endswith('.py'): + return + if not os.path.isdir(page_path): + logger.warning(f"Unexpected file at '{__user_path(page_path)}' will be ignored.") + return + for file_name in os.listdir(page_path): + file_path = os.path.join(page_path, file_name) + if not os.path.isfile(file_path): + logger.warning(f"Unexpected folder at '{__user_path(page_path)}' will be ignored.") + continue + if file_name == 'headers.json': + __verify_headers(file_path) + continue + file_name_split = file_name.split('.') + if len(file_name_split) < 2: + logger.warning(f"Could not deduce file extension at '{__user_path(file_path)}'.") + if file_name_split[-1] not in SUPPORTED_FILE_TYPES: + logger.warning(f"File type of '{__user_path(file_path)}' is not supported.") + + +def __verify_headers(path: str) -> None: + """ + Verifies whether the headers file at the given path is valid. + """ + with open(path, 'r', encoding='utf-8') as file: + try: + json_content = json.load(file) + except json.decoder.JSONDecodeError: + logger.warning(f"Could not parse '{__user_path(path)}'") + return + if not isinstance(json_content, list): + raise AttributeError(f"Not a list: '{__user_path(path)}'") + for item in json_content: + if 'key' and 'value' not in item: + logger.warning(f"Not all dictionary entries contain a key-value combination in '{__user_path(path)}'.") + + +def __user_path(path: str) -> str: + """ + Translates the given path to the user readeable path outside container. + """ + if path.startswith('/app/'): + return path[5:] + return path diff --git a/bughog/evaluation/experiment_result.py b/bughog/evaluation/experiment_result.py new file mode 100644 index 00000000..90c640c7 --- /dev/null +++ b/bughog/evaluation/experiment_result.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass(frozen=True) +class ExperimentResult: + executable_version: str + executable_origin: Optional[str] + state: dict + raw_results: dict + result_variables: dict[str, str] + is_dirty: bool + + @property + def poc_is_reproduced(self) -> bool: + for key, value in self.result_variables.items(): + if key.lower() == 'reproduced' and value.lower() == 'ok': + return True + return False + + def padded_subject_version(self) -> Optional[str]: + """ + Pads the executable's version. + Returns None if padding fails. + """ + padding_target = 4 + padded_version = [] + for sub in self.executable_version.split('.'): + if len(sub) > padding_target: + return None + padded_version.append('0' * (padding_target - len(sub)) + sub) + return '.'.join(padded_version) diff --git a/bughog/evaluation/file_structure.py b/bughog/evaluation/file_structure.py new file mode 100644 index 00000000..18ea4959 --- /dev/null +++ b/bughog/evaluation/file_structure.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import logging +import os + +logger = logging.getLogger(__name__) + + +class File: + def __init__(self, name: str, path: str): + self.name = name + self.path = path + + def __repr__(self): + return f'File(name={self.name}, path={self.path})' + + +class Folder: + __files_and_folders_to_ignore = ['.DS_Store'] + + def __init__(self, name: str, path: str): + self.name = name + self.path = path + self.subfolders: list[Folder] = [] + self.files: list[File] = [] + self.tags: list[str] = [] + + @classmethod + def parse(cls, path: str) -> Folder: + folder_name = os.path.basename(path) + folder = Folder(folder_name, path) + + try: + with os.scandir(path) as it: + for entry in it: + if entry.name in cls.__files_and_folders_to_ignore: + continue + elif entry.is_dir(): + folder.subfolders.append(Folder.parse(entry.path)) + elif entry.is_file(): + folder.files.append(File(entry.name, entry.path)) + except PermissionError: + logger.warning(f"Could not access folder '{path}', skipping.") + pass + + return folder + + def get_all_folders_with_tag(self, tag: str) -> list[Folder]: + all_folders_with_tag = [] + if tag in self.tags: + all_folders_with_tag.append(self) + for folder in self.subfolders: + all_folders_with_tag.extend(folder.get_all_folders_with_tag(tag)) + return all_folders_with_tag + + def __repr__(self): + return f'Folder(name={self.name}, path={self.path}, subfolders={self.subfolders}, files={self.files})' diff --git a/bci/browser/configuration/__init__.py b/bughog/integration_tests/__init__.py similarity index 100% rename from bci/browser/configuration/__init__.py rename to bughog/integration_tests/__init__.py diff --git a/bci/integration_tests/evaluation_configurations.py b/bughog/integration_tests/evaluation_configurations.py similarity index 67% rename from bci/integration_tests/evaluation_configurations.py rename to bughog/integration_tests/evaluation_configurations.py index 40851ab9..8c7903fb 100644 --- a/bci/integration_tests/evaluation_configurations.py +++ b/bughog/integration_tests/evaluation_configurations.py @@ -1,8 +1,14 @@ -from bci.evaluations.logic import BrowserConfiguration, EvaluationConfiguration, EvaluationParameters, EvaluationRange, SequenceConfiguration +from bughog.parameters import ( + EvaluationConfiguration, + EvaluationParameters, + EvaluationRange, + SequenceConfiguration, + SubjectConfiguration, +) -def get_default_browser_configuration(browser_name: str) -> BrowserConfiguration: - return BrowserConfiguration( +def get_default_browser_configuration(browser_name: str) -> SubjectConfiguration: + return SubjectConfiguration( browser_name, 'default', [], @@ -16,6 +22,7 @@ def get_default_evaluation_configuration() -> EvaluationConfiguration: 'terminal', ) + def get_default_evaluation_range(mech_group: str, only_releases: bool) -> EvaluationRange: return EvaluationRange( mech_group, @@ -24,6 +31,7 @@ def get_default_evaluation_range(mech_group: str, only_releases: bool) -> Evalua only_releases, ) + def get_default_sequence_config(sequence_limit: int) -> SequenceConfiguration: return SequenceConfiguration( 8, @@ -31,15 +39,19 @@ def get_default_sequence_config(sequence_limit: int) -> SequenceConfiguration: 'comp_search', ) -def get_default_evaluation_parameters(browser_name: str, mech_group: str, sequence_limit: int=50, only_releases: bool=True) -> EvaluationParameters: + +def get_default_evaluation_parameters( + browser_name: str, mech_group: str, sequence_limit: int = 50, only_releases: bool = True +) -> EvaluationParameters: return EvaluationParameters( get_default_browser_configuration(browser_name), get_default_evaluation_configuration(), get_default_evaluation_range(mech_group, only_releases), get_default_sequence_config(sequence_limit), - 'integrationtests_' + browser_name + 'integrationtests_' + browser_name, ) + def get_eval_parameters_list(mech_groups: list[str]) -> list[EvaluationParameters]: evaluation_parameters_list = [] for browser_name in ['chromium', 'firefox']: @@ -48,10 +60,6 @@ def get_eval_parameters_list(mech_groups: list[str]) -> list[EvaluationParameter sequence_limit = 999 else: sequence_limit = 50 - params = get_default_evaluation_parameters( - browser_name, - mech_group, - sequence_limit=sequence_limit - ) + params = get_default_evaluation_parameters(browser_name, mech_group, sequence_limit=sequence_limit) evaluation_parameters_list.append(params) return evaluation_parameters_list diff --git a/bci/integration_tests/verify_results.py b/bughog/integration_tests/verify_results.py similarity index 64% rename from bci/integration_tests/verify_results.py rename to bughog/integration_tests/verify_results.py index 2e3e5b20..311eda4e 100644 --- a/bci/integration_tests/verify_results.py +++ b/bughog/integration_tests/verify_results.py @@ -1,29 +1,29 @@ -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import EvaluationParameters -from bci.version_control.state_result_factory import StateResultFactory -from bci.version_control.states.state import State +from bughog.database.mongo.mongodb import MongoDB +from bughog.parameters import EvaluationParameters +from bughog.version_control.state.base import State def verify(evaluation_parameters_list: list[EvaluationParameters]) -> list: verification_results = [] for evaluation_parameters in evaluation_parameters_list: - experiment_name = evaluation_parameters.evaluation_range.mech_group + experiment_name = evaluation_parameters.evaluation_range.experiment_name verification_func = verification_functions()[experiment_name] - state_result_factory = StateResultFactory(experiment=experiment_name) - states = MongoDB().get_evaluated_states(evaluation_parameters, None, state_result_factory) + states = MongoDB().get_evaluated_states(evaluation_parameters, None) nb_of_success_results = len(list(filter(lambda x: verification_func(x) and not x.result.is_dirty, states))) nb_of_fail_results = len(list(filter(lambda x: not verification_func(x) and not x.result.is_dirty, states))) nb_of_error_results = len(list(filter(lambda x: x.result.is_dirty, states))) nb_of_results = nb_of_success_results + nb_of_fail_results + nb_of_error_results success_ratio = 0 if nb_of_results == 0 else round((nb_of_success_results / nb_of_results) * 100) - verification_results.append({ - 'experiment_name': experiment_name, - 'browser_name': evaluation_parameters.browser_configuration.browser_name, - 'nb_of_success_results': nb_of_success_results, - 'nb_of_fail_results': nb_of_fail_results, - 'nb_of_error_results': nb_of_error_results, - 'success_ratio': success_ratio, - }) + verification_results.append( + { + 'experiment_name': experiment_name, + 'browser_name': evaluation_parameters.subject_configuration.subject_name, + 'nb_of_success_results': nb_of_success_results, + 'nb_of_fail_results': nb_of_fail_results, + 'nb_of_error_results': nb_of_error_results, + 'success_ratio': success_ratio, + } + ) return verification_results @@ -43,5 +43,3 @@ def none_reproduced(state: State) -> bool: 'none_reproduced': none_reproduced, 'click': all_reproduced, } - - diff --git a/bci/main.py b/bughog/main.py similarity index 70% rename from bci/main.py rename to bughog/main.py index 833534c3..2b160c63 100644 --- a/bci/main.py +++ b/bughog/main.py @@ -1,25 +1,23 @@ import logging import time -import bci.database.mongo.container as mongodb_container -from bci.configuration import Global, Loggers -from bci.database.mongo.mongodb import MongoDB, ServerException -from bci.database.mongo.revision_cache import RevisionCache -from bci.distribution.worker_manager import WorkerManager -from bci.evaluations import experiments -from bci.evaluations.custom.custom_evaluation import CustomEvaluationFramework -from bci.evaluations.logic import ( +import bughog.database.mongo.container as mongodb_container +from bughog.configuration import Global, Loggers +from bughog.database.mongo.mongodb import MongoDB, ServerException +from bughog.distribution.worker_manager import WorkerManager +from bughog.parameters import ( DatabaseParameters, EvaluationParameters, - TestParameters, ) -from bci.search_strategy.bgb_search import BiggestGapBisectionSearch -from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence -from bci.search_strategy.composite_search import CompositeSearch -from bci.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy -from bci.version_control.state_factory import StateFactory -from bci.version_control.state_result_factory import StateResultFactory -from bci.web.clients import Clients +from bughog.search_strategy.bgb_search import BiggestGapBisectionSearch +from bughog.search_strategy.bgb_sequence import BiggestGapBisectionSequence +from bughog.search_strategy.composite_search import CompositeSearch +from bughog.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy +from bughog.subject.factory import create_subject +from bughog.subject.webbrowser.state_cache import PublicBrowserStateCache +from bughog.version_control.state.base import State +from bughog.version_control.state_factory import StateFactory +from bughog.web.clients import Clients logger = logging.getLogger(__name__) @@ -31,17 +29,13 @@ def __init__(self) -> None: self.stop_gracefully = False self.stop_forcefully = False - self.firefox_build = None - self.chromium_build = None - self.eval_queue = [] Global.initialize_folders() self.db_connection_params = Global.get_database_params() self.connect_to_database(self.db_connection_params) - RevisionCache.update() - experiments.verify() - self.evaluation_framework = CustomEvaluationFramework() + PublicBrowserStateCache.update() + logger.info('BugHog is ready!') def connect_to_database(self, db_connection_params: DatabaseParameters) -> None: @@ -60,7 +54,7 @@ def run(self, eval_params_list: list[EvaluationParameters]) -> None: for eval_params in eval_params_list: if self.stop_gracefully or self.stop_forcefully: break - self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'active') + self.__update_eval_queue(eval_params.evaluation_range.experiment_name, 'active') self.__update_state(is_running=True, reason='user', status='running', queue=self.eval_queue) self.run_single_evaluation(eval_params, worker_manager) @@ -91,58 +85,57 @@ def run_single_evaluation(self, eval_params: EvaluationParameters, worker_manage nb_of_iterations = 3 for i in range(1, nb_of_iterations + 1): start_time = time.time() - browser_name = eval_params.browser_configuration.browser_name - experiment_name = eval_params.evaluation_range.mech_group + browser_name = eval_params.subject_configuration.subject_name + experiment_name = eval_params.evaluation_range.experiment_name search_strategy = self.create_sequence_strategy(eval_params) - logger.info(f"Starting evaluation for experiment '{experiment_name}' with browser '{browser_name}', iteration {i}/{nb_of_iterations}.") + logger.info( + f"Starting evaluation for experiment '{experiment_name}' with browser '{browser_name}', iteration {i}/{nb_of_iterations}." + ) try: while (self.stop_gracefully or self.stop_forcefully) is False: # Update search strategy with new potentially new results current_state = search_strategy.next() - # Prepare worker parameters - worker_params = eval_params.create_worker_params_for(current_state, self.db_connection_params) - # Start worker to perform evaluation - worker_manager.start_test(worker_params) + worker_manager.start_experiment(eval_params, current_state) except SequenceFinished: iteration_time = round(time.time() - start_time) worker_manager.wait_until_all_evaluations_are_done() - logger.debug(f"Last experiment has finished for iteration {i}/{nb_of_iterations}. This iteration took {iteration_time}s.") + logger.debug( + f'Last experiment has finished for iteration {i}/{nb_of_iterations}. This iteration took {iteration_time}s.' + ) # Retry all tests with a dirty result once. self.retry_dirty_tests(eval_params, worker_manager) self.state['reason'] = 'finished' - self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'done') + self.__update_eval_queue(eval_params.evaluation_range.experiment_name, 'done') def retry_dirty_tests(self, eval_params: EvaluationParameters, worker_manager: WorkerManager) -> None: - state_result_factory = StateResultFactory() - dirty_states = MongoDB().get_evaluated_states(eval_params, None, state_result_factory, dirty=True) + dirty_states = MongoDB().get_evaluated_states(eval_params, None, dirty=True) if (nb_of_dirty_states := len(dirty_states)) == 0: - logger.info("No tests are associated with a dirty result.") + logger.info('No tests are associated with a dirty result.') return - logger.info(f"Retrying {nb_of_dirty_states} tests with a dirty result...") + logger.info(f'Retrying {nb_of_dirty_states} tests with a dirty result...') for dirty_state in dirty_states: if self.stop_gracefully or self.stop_forcefully: return - worker_params = eval_params.create_worker_params_for(dirty_state, self.db_connection_params) - test_params = worker_params.create_test_params() - MongoDB().remove_datapoint(test_params) - worker_manager.start_test(worker_params) + MongoDB().remove_datapoint(eval_params, dirty_state) + worker_manager.start_experiment(eval_params, dirty_state) worker_manager.wait_until_all_evaluations_are_done() - dirty_states_after_retry = MongoDB().get_evaluated_states(eval_params, None, state_result_factory, dirty=True) - logger.info(f"Dirty test results reduced from {nb_of_dirty_states} to {len(dirty_states_after_retry)}.") + dirty_states_after_retry = MongoDB().get_evaluated_states(eval_params, None, dirty=True) + logger.info(f'Dirty test results reduced from {nb_of_dirty_states} to {len(dirty_states_after_retry)}.') @staticmethod def create_sequence_strategy(eval_params: EvaluationParameters) -> SequenceStrategy: sequence_config = eval_params.sequence_configuration search_strategy = sequence_config.search_strategy sequence_limit = sequence_config.sequence_limit - state_factory = StateFactory(eval_params) + subject = create_subject(eval_params) + state_factory = StateFactory(subject, eval_params) if search_strategy == 'bgb_sequence': strategy = BiggestGapBisectionSequence(state_factory, sequence_limit) @@ -155,19 +148,17 @@ def create_sequence_strategy(eval_params: EvaluationParameters) -> SequenceStrat return strategy def activate_stop_gracefully(self): - if self.evaluation_framework: + if self.state['is_running']: self.stop_gracefully = True self.__update_state(is_running=True, reason='user', status='waiting_to_stop') - self.evaluation_framework.stop_gracefully() logger.info('Received user signal to gracefully stop.') else: logger.info('Received user signal to gracefully stop, but no evaluation is running.') def activate_stop_forcefully(self) -> None: - if self.evaluation_framework: + if self.state['is_running']: self.stop_forcefully = True self.__update_state(is_running=True, reason='user', status='waiting_to_stop') - self.evaluation_framework.stop_gracefully() WorkerManager.forcefully_stop_all_running_containers() logger.info('Received user signal to forcefully stop.') else: @@ -200,8 +191,8 @@ def push_info(self, ws, *args) -> None: update['state'] = self.state Clients.push_info(ws, update) - def remove_datapoint(self, params: TestParameters) -> None: - MongoDB().remove_datapoint(params) + def remove_datapoint(self, params: EvaluationParameters, state: State) -> None: + MongoDB().remove_datapoint(params, state) Clients.push_results_to_all() def __update_state(self, **kwargs) -> None: @@ -212,10 +203,7 @@ def __update_state(self, **kwargs) -> None: def __init_eval_queue(self, eval_params_list: list[EvaluationParameters]) -> None: self.eval_queue = [] for eval_params in eval_params_list: - self.eval_queue.append({ - 'experiment': eval_params.evaluation_range.mech_group, - 'state': 'pending' - }) + self.eval_queue.append({'experiment': eval_params.evaluation_range.experiment_name, 'state': 'pending'}) def __update_eval_queue(self, experiment: str, state: str) -> None: for eval in self.eval_queue: diff --git a/bughog/parameters.py b/bughog/parameters.py new file mode 100644 index 00000000..bd88c57f --- /dev/null +++ b/bughog/parameters.py @@ -0,0 +1,329 @@ +from __future__ import annotations + +import base64 +import json +import logging +from dataclasses import asdict, dataclass +import pickle +from typing import Literal, Optional + +from werkzeug.datastructures import ImmutableMultiDict + + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class EvaluationParameters: + """ + All parameters required to define an evaluation. + """ + + subject_configuration: SubjectConfiguration + evaluation_configuration: EvaluationConfiguration + evaluation_range: EvaluationRange + sequence_configuration: SequenceConfiguration + database_params: DatabaseParameters + + # def create_experiment_params(self, state_params: StateParameters) -> ExperimentParameters: + # return ExperimentParameters( + # self.subject_configuration, + # self.evaluation_configuration, + # state_params, + # self.evaluation_range.experiment_name, + # self.database_params, + # ) + + def serialize(self) -> str: + pickled_bytes = pickle.dumps(self, pickle.HIGHEST_PROTOCOL) + return base64.b64encode(pickled_bytes).decode('ascii') + + @staticmethod + def deserialize(pickled_str: str) -> EvaluationParameters: + pickled_bytes = base64.b64decode(pickled_str) + return pickle.loads(pickled_bytes) + +@dataclass(frozen=True) +class SubjectConfiguration: + subject_type: str + subject_name: str + subject_setting: str + cli_options: list[str] + extensions: list[str] + + def to_dict(self) -> dict: + return asdict(self) + + @staticmethod + def from_dict(data: dict) -> SubjectConfiguration: + return SubjectConfiguration(data['subject_type'], data['subject_name'], data['subject_setting'], data['cli_options'], data['extensions']) + + +@dataclass(frozen=True) +class EvaluationConfiguration: + project: str + automation: str + seconds_per_visit: int = 5 + + def to_dict(self) -> dict: + return asdict(self) + + @staticmethod + def from_dict(data: dict) -> EvaluationConfiguration: + return EvaluationConfiguration(data['project'], data['automation'], data['seconds_per_visit']) + + +@dataclass(frozen=True) +class EvaluationRange: + experiment_name: str + major_version_range: tuple[int, int] | None = None + revision_number_range: tuple[int, int] | None = None + only_release_revisions: bool = False + + def __post_init__(self): + if self.major_version_range: + assert self.major_version_range[0] <= self.major_version_range[1] + elif self.revision_number_range: + assert self.revision_number_range[0] <= self.revision_number_range[1] + else: + raise AttributeError('Evaluation ranges require either major versions or revision numbers') + + +@dataclass(frozen=True) +class SequenceConfiguration: + nb_of_containers: int = 8 + sequence_limit: int = 10000 + search_strategy: str | None = None + + +@dataclass(frozen=True) +class DatabaseParameters: + host: str + username: str + password: str + database_name: str + binary_cache_limit: int + + def to_dict(self) -> dict: + return asdict(self) + + @staticmethod + def from_dict(data: dict) -> DatabaseParameters: + return DatabaseParameters( + data['host'], + data['username'], + data['password'], + data['database_name'], + data['binary_cache_limit'], + ) + + def __str__(self) -> str: + return f'{self.username}@{self.host}:27017/{self.database_name}' + + +# @dataclass(frozen=True) +# class ExperimentParameters: +# """ +# Parameters that define a single experiment. +# """ +# subject_configuration: SubjectConfiguration +# evaluation_configuration: EvaluationConfiguration +# state_params: StateParameters +# experiment: str +# database_params: DatabaseParameters + +# def _to_dict(self): +# return { +# 'subject_configuration': self.subject_configuration.to_dict(), +# 'evaluation_configuration': self.evaluation_configuration.to_dict(), +# 'state': self.state_params, +# 'experiment': self.experiment, +# 'database_params': self.database_params, +# } + +# def create_test_result_with( +# self, subject_version: str, binary_origin: str, data: dict, dirty: bool +# ) -> ExperimentResult: +# return ExperimentResult(self, subject_version, binary_origin, data, dirty) + +# @staticmethod +# def from_dict(data) -> Optional[ExperimentParameters]: +# if data is None: +# return None +# subject_configuration = SubjectConfiguration.from_dict(data) +# evaluation_configuration = EvaluationConfiguration.from_dict(data) +# state = StateParameters.from_dict(data) +# experiment = data['experiment'] +# database_collection = data['db_collection'] +# return ExperimentParameters( +# subject_configuration, evaluation_configuration, state, experiment, database_collection +# ) + +# def serialize(self) -> str: +# return json.dumps(self._to_dict()) + +# def __repr__(self) -> str: +# param_dict = self._to_dict() +# # Mask password +# param_dict['database_connection_params']['password'] = '*' +# return json.dumps(param_dict) + +# @staticmethod +# def get_database_params(string: str) -> DatabaseParameters: +# data = json.loads(string) +# return DatabaseParameters.from_dict(data['database_connection_params']) + +# @staticmethod +# def deserialize(string: str) -> ExperimentParameters: +# data = json.loads(string) +# subject_config = SubjectConfiguration.from_dict(data['subject_configuration']) +# eval_config = EvaluationConfiguration.from_dict(data['evaluation_configuration']) +# state = StateParameters.from_dict(data['state']) +# experiment = data['experiment'] +# database_params = DatabaseParameters.from_dict(data['database_params']) +# return ExperimentParameters(subject_config, eval_config, state, experiment, database_params) + +# def __str__(self) -> str: +# return f'Eval({self.state_params}: [{", ".join(self.experiment)}])' + + +# @dataclass(frozen=True) +# class ExperimentResult: +# params: ExperimentParameters +# subject_version: str +# binary_origin: str +# data: dict +# is_dirty: bool = False +# driver_version: str | None = None + +# @property +# def padded_subject_version(self): +# padding_target = 4 +# padded_version = [] +# for sub in self.subject_version.split('.'): +# if len(sub) > padding_target: +# raise AttributeError(f"Version '{self.subject_version}' is too big to be padded") +# padded_version.append('0' * (padding_target - len(sub)) + sub) +# return '.'.join(padded_version) + + +# @dataclass(frozen=True) +# class PlotParameters: +# experiment: Optional[str] +# subject_name: Optional[str] +# database_collection: Optional[str] +# major_version_range: Optional[tuple[int, int]] = None +# revision_number_range: Optional[tuple[int, int]] = None +# subject_config: str = 'default' +# cli_options: Optional[list[str]] = None +# dirty_allowed: bool = True + +# @staticmethod +# def from_dict(data: dict) -> PlotParameters: +# if data.get('lower_version', None) and data.get('upper_version', None): +# major_version_range = (data['lower_version'], data['upper_version']) +# else: +# major_version_range = None +# if data.get('lower_revision_nb', None) and data.get('upper_revision_nb', None): +# revision_number_range = ( +# data['lower_revision_nb'], +# data['upper_revision_nb'], +# ) +# else: +# revision_number_range = None +# return PlotParameters( +# data.get('plot_experiment', None), +# data.get('target_mech_id', None), +# data.get('subject_name', None), +# data.get('db_collection', None), +# major_version_range=major_version_range, +# revision_number_range=revision_number_range, +# subject_config=data.get('subject_setting', 'default'), +# cli_options=data.get('cli_options', []), +# dirty_allowed=data.get('dirty_allowed', True), +# ) + + +# @dataclass(frozen=True) +# class StateParameters: +# type: Literal['release', 'commit'] +# version_or_commit: int + +# def __post_init__(self): +# if self.type not in ('release', 'commit'): +# raise AttributeError("Type should be either 'release' or 'commit'") + +# def to_dict(self) -> dict: +# return asdict(self) + +# @staticmethod +# def from_dict(data) -> StateParameters: +# return StateParameters(data['type'], data['version_or_commit']) + + +@staticmethod +def evaluation_factory(kwargs: ImmutableMultiDict) -> list[EvaluationParameters]: + experiments = kwargs.get('tests') + if experiments is None: + raise MissingParametersException() + + subject_configuration = SubjectConfiguration.from_dict(kwargs) + evaluation_configuration = EvaluationConfiguration( + kwargs['project'], kwargs['automation'], int(kwargs.get('seconds_per_visit', 5)) + ) + sequence_configuration = SequenceConfiguration( + int(kwargs.get('nb_of_containers')), + int(kwargs.get('sequence_limit')), + kwargs.get('search_strategy'), + ) + evaluation_params_list = [] + for experiment in experiments: + evaluation_range = EvaluationRange( + experiment, + __get_version_range(kwargs), + __get_revision_number_range(kwargs), + kwargs.get('only_release_revisions', False), + ) + database_collection = kwargs.get('db_collection') + evaluation_params = EvaluationParameters( + subject_configuration, + evaluation_configuration, + evaluation_range, + sequence_configuration, + database_collection, + ) + evaluation_params_list.append(evaluation_params) + return evaluation_params_list + + +@staticmethod +def __get_cookie_name(form_data: dict[str, str]) -> str | None: + if form_data['check_for'] == 'request': + return None + if 'cookie_name' in form_data: + return form_data['cookie_name'] + return 'generic' + + +@staticmethod +def __get_version_range(form_data: dict[str, str]) -> tuple[int, int] | None: + lower_version = form_data.get('lower_version', None) + upper_version = form_data.get('upper_version', None) + lower_version = int(lower_version) if lower_version else None + upper_version = int(upper_version) if upper_version else None + assert (lower_version is None) == (upper_version is None) + return (lower_version, upper_version) if lower_version is not None else None + + +@staticmethod +def __get_revision_number_range(form_data: dict[str, str]) -> tuple[int, int] | None: + lower_rev_number = form_data.get('lower_revision_nb', None) + upper_rev_number = form_data.get('upper_revision_nb', None) + lower_rev_number = int(lower_rev_number) if lower_rev_number else None + upper_rev_number = int(upper_rev_number) if upper_rev_number else None + assert (lower_rev_number is None) == (upper_rev_number is None) + return (lower_rev_number, upper_rev_number) if lower_rev_number is not None else None + + +class MissingParametersException(Exception): + pass diff --git a/bci/browser/interaction/__init__.py b/bughog/search_strategy/__init__.py similarity index 100% rename from bci/browser/interaction/__init__.py rename to bughog/search_strategy/__init__.py diff --git a/bci/search_strategy/bgb_search.py b/bughog/search_strategy/bgb_search.py similarity index 95% rename from bci/search_strategy/bgb_search.py rename to bughog/search_strategy/bgb_search.py index dfa3e949..53244648 100644 --- a/bci/search_strategy/bgb_search.py +++ b/bughog/search_strategy/bgb_search.py @@ -3,10 +3,10 @@ import logging from typing import Optional -from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence -from bci.search_strategy.sequence_strategy import SequenceFinished -from bci.version_control.state_factory import StateFactory -from bci.version_control.states.state import State +from bughog.search_strategy.bgb_sequence import BiggestGapBisectionSequence +from bughog.search_strategy.sequence_strategy import SequenceFinished +from bughog.version_control.state_factory import StateFactory +from bughog.version_control.state.base import State logger = logging.getLogger(__name__) diff --git a/bci/search_strategy/bgb_sequence.py b/bughog/search_strategy/bgb_sequence.py similarity index 95% rename from bci/search_strategy/bgb_sequence.py rename to bughog/search_strategy/bgb_sequence.py index 4fb19a4c..fb0493b4 100644 --- a/bci/search_strategy/bgb_sequence.py +++ b/bughog/search_strategy/bgb_sequence.py @@ -1,9 +1,9 @@ import logging from typing import Optional -from bci.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy -from bci.version_control.state_factory import StateFactory -from bci.version_control.states.state import State +from bughog.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy +from bughog.version_control.state_factory import StateFactory +from bughog.version_control.state.base import State logger = logging.getLogger(__name__) diff --git a/bci/search_strategy/composite_search.py b/bughog/search_strategy/composite_search.py similarity index 75% rename from bci/search_strategy/composite_search.py rename to bughog/search_strategy/composite_search.py index 5dffc7c6..2312137b 100644 --- a/bci/search_strategy/composite_search.py +++ b/bughog/search_strategy/composite_search.py @@ -1,9 +1,9 @@ from typing import Optional -from bci.search_strategy.bgb_search import BiggestGapBisectionSearch -from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence -from bci.search_strategy.sequence_strategy import SequenceFinished -from bci.version_control.state_factory import StateFactory -from bci.version_control.states.state import State +from bughog.search_strategy.bgb_search import BiggestGapBisectionSearch +from bughog.search_strategy.bgb_sequence import BiggestGapBisectionSequence +from bughog.search_strategy.sequence_strategy import SequenceFinished +from bughog.version_control.state_factory import StateFactory +from bughog.version_control.state.base import State class CompositeSearch(): diff --git a/bci/search_strategy/sequence_strategy.py b/bughog/search_strategy/sequence_strategy.py similarity index 89% rename from bci/search_strategy/sequence_strategy.py rename to bughog/search_strategy/sequence_strategy.py index f112dbfe..be7d111f 100644 --- a/bci/search_strategy/sequence_strategy.py +++ b/bughog/search_strategy/sequence_strategy.py @@ -3,14 +3,14 @@ from concurrent.futures import ThreadPoolExecutor from typing import Optional -from bci.version_control.state_factory import StateFactory -from bci.version_control.states.state import State +from bughog.version_control.state_factory import StateFactory +from bughog.version_control.state.base import State logger = logging.getLogger(__name__) class SequenceStrategy: - def __init__(self, state_factory: StateFactory, limit: int, completed_states: Optional[list[State]]=None) -> None: + def __init__(self, state_factory: StateFactory, limit: int, completed_states: Optional[list[State]] = None) -> None: """ Initializes the sequence strategy. @@ -28,7 +28,7 @@ def next(self) -> State: pass def is_available(self, state: State) -> bool: - return state.has_available_binary() + return state.has_available_executable() def _add_state(self, elem: State) -> None: """ @@ -63,7 +63,7 @@ def _find_closest_state_with_available_binary(self, target: State, boundaries: t """ Finds the closest state with an available binary **strictly** within the given boundaries. """ - if target.has_available_binary(): + if target.has_available_executable(): return target try: @@ -76,7 +76,7 @@ def _find_closest_state_with_available_binary(self, target: State, boundaries: t def index_has_available_binary(index: int) -> Optional[State]: state = self._state_factory.create_state(index) - if state.has_available_binary(): + if state.has_available_executable(): return state else: return None @@ -87,7 +87,7 @@ def index_has_available_binary(index: int) -> Optional[State]: while (best_splitter_index - diff) > first_state.index or (best_splitter_index + diff) < last_state.index: with ThreadPoolExecutor(max_workers=6) as executor: futures = [] - for offset in (-diff, diff, - 1 - diff, 1 + diff, - 2 - diff, 2 + diff): + for offset in (-diff, diff, -1 - diff, 1 + diff, -2 - diff, 2 + diff): target_index = best_splitter_index + offset if first_state.index < target_index < last_state.index: futures.append(executor.submit(index_has_available_binary, target_index)) @@ -100,7 +100,6 @@ def index_has_available_binary(index: int) -> Optional[State]: diff += 2 return None - def __get_closest_available_state(self, target: State, boundaries: tuple[State, State]) -> State | None: """ Return the closest state with an available binary. @@ -108,10 +107,10 @@ def __get_closest_available_state(self, target: State, boundaries: tuple[State, try: states = target.get_previous_and_next_state_with_binary() states = [state for state in states if state is not None] - ordered_states = sorted(states, key=lambda x: abs(target.revision_nb - x.revision_nb)) + ordered_states = sorted(states, key=lambda x: abs(target.commit_nb - x.commit_nb)) for state in ordered_states: - if boundaries[0].revision_nb < state.revision_nb < boundaries[1].revision_nb: + if boundaries[0].commit_nb < state.commit_nb < boundaries[1].commit_nb: return state return None @@ -123,5 +122,6 @@ def __get_closest_available_state(self, target: State, boundaries: tuple[State, class SequenceFinished(Exception): pass + class FunctionalityNotAvailable(Exception): pass diff --git a/bci/database/__init__.py b/bughog/subject/__init__.py similarity index 100% rename from bci/database/__init__.py rename to bughog/subject/__init__.py diff --git a/bughog/subject/base.py b/bughog/subject/base.py new file mode 100644 index 00000000..5884c19b --- /dev/null +++ b/bughog/subject/base.py @@ -0,0 +1,62 @@ +""" +This module provides abstract base classes for subjects, states and executables. + +All classes should be implemented by newly added subjects. +""" + +from __future__ import annotations + +import logging +import os +from abc import ABC, abstractmethod + +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.state.commit.base import CommitState +from bughog.version_control.state.release.base import ReleaseState + +logger = logging.getLogger(__name__) + + +class Subject(ABC): + """ + Abstract base class representing an evaluation target. + + The Subject class defines the interface and common functionality for any suibject that can be evaluated. + """ + + @staticmethod + @abstractmethod + def type() -> str: + pass + + @staticmethod + @abstractmethod + def name() -> str: + pass + + @staticmethod + @abstractmethod + def state_oracle() -> type[StateOracle]: + pass + + @staticmethod + @abstractmethod + def release_state_class() -> type[ReleaseState]: + pass + + @staticmethod + @abstractmethod + def commit_state_class() -> type[CommitState]: + pass + + @property + def assets_folder_path(self) -> str: + return os.path.join('/app/subject', self.type(), self.name()) + + @staticmethod + @abstractmethod + def get_availability() -> dict: + """ + Returns availability data (minimum and maximu, release versions, and configuration options) of the subject. + """ + pass diff --git a/bughog/subject/evaluation_framework.py b/bughog/subject/evaluation_framework.py new file mode 100644 index 00000000..cc2e0c6b --- /dev/null +++ b/bughog/subject/evaluation_framework.py @@ -0,0 +1,39 @@ +import logging +import os +from abc import abstractmethod + +from bughog.evaluation.collectors.collector import Collector + +logger = logging.getLogger(__name__) + + +class EvaluationFramework: + def __init__(self, subject_type: str) -> None: + self.experiment_root_folder = os.path.join('/app/subject/', subject_type, 'experiments') + if not os.path.isdir(self.experiment_root_folder): + raise AttributeError(f"Could not open '{self.experiment_root_folder}'.") + self.collector = self.get_collector() + + @abstractmethod + def get_collector(self) -> Collector: + pass + + @abstractmethod + def experiment_is_valid(self, project: str, experiment: str) -> bool: + pass + + @abstractmethod + def create_empty_experiment(self, project: str, experiment: str): + pass + + @abstractmethod + def execute_script_command(self, command: str): + pass + + def get_default_file_content(self, file_type: str) -> bytes: + default_file_content_file = os.path.join(self.experiment_root_folder, '_default_files', file_type) + if not os.path.isdir(default_file_content_file): + logger.warning(f"Could not find default file content for file type '{file_type}'.") + return b'' + with open(default_file_content_file, 'rb') as file: + return file.read() diff --git a/bughog/subject/executable.py b/bughog/subject/executable.py new file mode 100644 index 00000000..8fb1af27 --- /dev/null +++ b/bughog/subject/executable.py @@ -0,0 +1,169 @@ +import logging +import os +import signal +import subprocess +import time +from abc import ABC, abstractmethod +from enum import Enum, auto, unique +from typing import Optional + +from bughog import util +from bughog.parameters import SubjectConfiguration +from bughog.version_control.state.base import State + +logger = logging.getLogger(__name__) + + +class Executable(ABC): + """ + Abstract base class representing a subject executable, which is executed during evaluation. + """ + + def __init__(self, config: SubjectConfiguration, state: State) -> None: + self.config = config + self.state = state + self.__version = None + self.origin = None + self.status = ExecutableStatus.NEW + self.error_message = None + + @property + @abstractmethod + def executable_name(self) -> str: + pass + + @property + @abstractmethod + def log_path(self) -> Optional[str]: + pass + + @property + def executable_folder(self) -> str: + return os.path.join('/tmp', f'{self.config.subject_name}-{str(self.state.index)}') + + @property + def executable_path(self) -> str: + return os.path.join(self.executable_folder, self.executable_name) + + @property + def is_ready_for_use(self) -> bool: + return os.path.isfile(self.executable_path) and self.version is not None + + @property + def version(self) -> str: + if self.__version is None: + self.__version = self._get_version() + return self.__version + + @abstractmethod + def _configure_executable(self): + """ + Configures the downloaded executable folder after download and extraction, but before it is cached or used. + This function should be idempotent. + """ + pass + + def download(self): + from bughog.database.mongo.executable_cache import ExecutableCache + if self.is_ready_for_use: + logger.info('Executable with index {self.state.index} is already ready for use.') + elif ExecutableCache.fetch_executable_files(self.config, self.state, self.executable_path): + logger.info(f'Executable with index {self.state.index} was fetched from cache.') + elif not self.state.has_publicly_available_executable(): + raise Exception(f'Executable with index {self.state.index} is not available online.') + else: + start = time.time() + executable_urls = self.state.get_executable_source_urls() + util.download_and_extract(executable_urls, self.executable_folder) + elapsed_time = time.time() - start + logger.info(f'Executable for {self.state.index} downloaded in {elapsed_time:.2f}s') + self._configure_executable() + ExecutableCache.store_executable_files(self.config, self.state, self.executable_folder) + + @property + @abstractmethod + def supported_options(self) -> list[str]: + pass + + @abstractmethod + def _get_version(self) -> str: + """ + Runs the executable to retrieve its version string. + """ + pass + + @abstractmethod + def pre_evaluation_setup(self): + """ + Executes the setup required for an evaluation. + """ + pass + + @abstractmethod + def pre_experiment_setup(self): + """ + Executes the setup required for an experiment. + """ + pass + + @abstractmethod + def pre_try_setup(self): + """ + Executes the setup required for a try. + """ + pass + + @abstractmethod + def post_try_cleanup(self): + """ + Executes the cleanup required after a try. + """ + pass + + @abstractmethod + def post_experiment_cleanup(self): + """ + Executes the cleanup required after an experiment. + """ + pass + + @abstractmethod + def post_evaluation_cleanup(self): + """ + Executes the cleanup required after an evaluation. + """ + pass + + def run(self, args: list[str], timeout: int = 5): + """ + Runs the executable with the given arguments, and kills it after the given timeout. + """ + logger.debug(f'Executing: {self.executable_path} {" ".join(args)}') + with open('/tmp/app.log', 'a+') as file: + proc = subprocess.Popen(args, stdout=file, stderr=file) + time.sleep(timeout) + logger.debug('Terminating browser process using SIGINT...') + # Use SIGINT and SIGTERM to ensure that cookies remain saved in a browser subjects. + proc.send_signal(signal.SIGINT) + proc.send_signal(signal.SIGTERM) + try: + stdout, stderr = proc.communicate(timeout=5) + except subprocess.TimeoutExpired: + logger.debug('App process did not terminate after 5s. Killing process through pkill...') + subprocess.run(['pkill', '-2', args[0].split('/')[-1]]) + proc.wait() + logger.debug('Browser process terminated.') + return proc + + +@unique +class ExecutableStatus(Enum): + """ + The condition of an executable. + """ + + COMPLETED = auto() + READY_FOR_USE = auto() + EXPERIMENT_FAILED = auto() + UNAVAILABLE = auto() + NEW = auto() diff --git a/bughog/subject/factory.py b/bughog/subject/factory.py new file mode 100644 index 00000000..7444abe3 --- /dev/null +++ b/bughog/subject/factory.py @@ -0,0 +1,95 @@ +from functools import lru_cache + +from bughog.evaluation.collectors.requests import RequestCollector +from bughog.evaluation.experiment import Experiments +from bughog.parameters import EvaluationParameters +from bughog.subject.base import Subject +from bughog.subject.evaluation_framework import EvaluationFramework +from bughog.subject.state_oracle import StateOracle +from bughog.subject.webbrowser.chromium.subject import Chromium +from bughog.subject.webbrowser.evaluation import BrowserEvaluationFramework +from bughog.subject.webbrowser.firefox.subject import Firefox +from bughog.version_control.state.release.base import ReleaseState + +subjects = { + 'webbrowser': { + 'collectors': [ + RequestCollector + ], + 'evaluation_framework': BrowserEvaluationFramework, + 'subjects': [ + Chromium, + Firefox, + ] + } +} + + +@staticmethod +def get_all_subject_types() -> list[str]: + return sorted(subjects.keys()) + + +@staticmethod +def get_all_subjects_for(subject_type: str) -> list[type[Subject]]: + if subject_classes := subjects.get(subject_type): + return subject_classes['subjects'] + raise AttributeError(f"Subject type '{subject_type}' is not supported.") + + +@staticmethod +def get_all_subject_names_for(subject_type: str) -> list[str]: + return [subject.name() for subject in get_all_subjects_for(subject_type)] + + +@staticmethod +def create_evaluation_framework(subject_type: str) -> EvaluationFramework: + if subject_classes := subjects.get(subject_type): + return subject_classes['evaluation_framework'](subject_type) + raise AttributeError(f"Subject type '{subject_type}' is not supported.") + + +@lru_cache(maxsize=10) +def create_experiments(subject_type: str) -> Experiments: + return Experiments(subject_type, create_evaluation_framework(subject_type)) + + +@staticmethod +def create_subject(params: EvaluationParameters) -> Subject: + type = params.subject_configuration.subject_type + name = params.subject_configuration.subject_name + return get_subject_class(type)(name) + + +@staticmethod +def get_subject_availability() -> list[dict]: + subject_availability = [] + for subject_type in get_all_subject_types(): + subjects_for_type = {'subject_type': subject_type, 'subjects': []} + for subject in get_all_subjects_for(subject_type): + subjects_for_type['subjects'].append(subject.get_availability()) + subject_availability.append(subjects_for_type) + return subject_availability + + +@staticmethod +def get_subject_availability_for(type: str, name: str) -> dict: + return get_subject_class(type, name).get_availability() + + +@staticmethod +def get_subject_class(subject_type: str, subject_name: str) -> type[Subject]: + if available_subjects := subjects.get(subject_type): + if subject_class := available_subjects.get(subject_name): + return subject_class + raise AttributeError(f"Subject '{subject_type} {subject_name}' is not supported.") + + +@staticmethod +def get_release_state_class(subject_type: str, subject_name: str) -> type[ReleaseState]: + return get_subject_class(subject_type, subject_name).release_state_class() + + +@staticmethod +def get_state_oracle(subject_type: str, subject_name: str) -> type[StateOracle]: + return get_subject_class(subject_type, subject_name).state_oracle() diff --git a/bughog/subject/interaction.py b/bughog/subject/interaction.py new file mode 100644 index 00000000..f13b535c --- /dev/null +++ b/bughog/subject/interaction.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod + +from bughog.subject.executable import Executable + +class Interaction(ABC): + + def __init__(self, executable: Executable, script: list[str], params: ExperimentParameters) -> None: + self.executable = executable + self.script = script + self.params = params + + def execute(self) -> None: + self._do_experiment() + self._do_sanity_check() + + @abstractmethod + def _do_experiment(self) -> None: + pass + + @abstractmethod + def _do_sanity_check(self) -> None: + pass diff --git a/bughog/subject/state_oracle.py b/bughog/subject/state_oracle.py new file mode 100644 index 00000000..560aa294 --- /dev/null +++ b/bughog/subject/state_oracle.py @@ -0,0 +1,28 @@ +from abc import ABC, abstractmethod + + +class StateOracle(ABC): + + @abstractmethod + def find_commit_nb(self, commit_id: str) -> int: + pass + + @abstractmethod + def find_commit_id(self, commit_nb: int) -> str: + pass + + @abstractmethod + def has_publicly_available_release_executable(self, major_version: int) -> bool: + pass + + @abstractmethod + def get_release_executable_download_urls(self, major_version: int) -> list[str]: + pass + + @abstractmethod + def has_publicly_available_commit_executable(self, commit_nb: int) -> bool: + pass + + @abstractmethod + def get_commit_executable_download_urls(self, commit_nb: int) -> list[str]: + pass diff --git a/bci/database/mongo/__init__.py b/bughog/subject/webbrowser/__init__.py similarity index 100% rename from bci/database/mongo/__init__.py rename to bughog/subject/webbrowser/__init__.py diff --git a/bughog/subject/webbrowser/base.py b/bughog/subject/webbrowser/base.py new file mode 100644 index 00000000..9353c200 --- /dev/null +++ b/bughog/subject/webbrowser/base.py @@ -0,0 +1,13 @@ +from abc import ABC + +from bughog.subject.base import Subject + + +class Browser(Subject, ABC): + def __init__(self) -> None: + super().__init__() + + @staticmethod + def type(): + return 'webbrowser' + diff --git a/bci/evaluations/__init__.py b/bughog/subject/webbrowser/binary/__init__.py similarity index 100% rename from bci/evaluations/__init__.py rename to bughog/subject/webbrowser/binary/__init__.py diff --git a/bci/browser/binary/artisanal_manager.py b/bughog/subject/webbrowser/binary/artisanal_manager.py similarity index 57% rename from bci/browser/binary/artisanal_manager.py rename to bughog/subject/webbrowser/binary/artisanal_manager.py index de07b4e8..d55f9628 100644 --- a/bci/browser/binary/artisanal_manager.py +++ b/bughog/subject/webbrowser/binary/artisanal_manager.py @@ -1,16 +1,16 @@ import json import logging import os -from bci import cli -from bci.version_control.states.state import State + +from bughog import cli +from bughog.version_control.states.base import State logger = logging.getLogger('bci') -META_FILE_NAME = "meta.json" +META_FILE_NAME = 'meta.json' class ArtisanalBuildManager: - def __init__(self, bin_folder_path: str, executable_name: str) -> None: self.builds_folder_path = os.path.join(bin_folder_path, 'artisanal') self.executable_name = executable_name @@ -26,25 +26,33 @@ def update(self): self._overwrite_meta_info() def get_artisanal_binaries_list(self) -> list: - return sorted(self.meta_info, key=lambda i: int(i["id"])) + return sorted(self.meta_info, key=lambda i: int(i['id'])) def has_artisanal_binary_for(self, state: State) -> bool: - return len(list(filter(lambda x: x['id'] == state.revision_nb, self.meta_info))) > 0 + return len(list(filter(lambda x: x['id'] == state.commit_nb, self.meta_info))) > 0 def add_new_subfolders(self, subfolders): - logger.info("Adding new subfolders to metadata") - new_subfolders = [subfolder for subfolder in subfolders if subfolder not in [entry["folder"] for entry in self.meta_info if "folder" in entry]] + logger.info('Adding new subfolders to metadata') + new_subfolders = [ + subfolder + for subfolder in subfolders + if subfolder not in [entry['folder'] for entry in self.meta_info if 'folder' in entry] + ] for subfolder in new_subfolders: self._add_entry(subfolder) def remove_deleted_subfolders(self, subfolders): - logger.info("Removing deleted subfolders from metadata") - deleted_subfolders = [subfolder for subfolder in [entry["folder"] for entry in self.meta_info if "folder" in entry] if subfolder not in subfolders] - self.meta_info = [entry for entry in self.meta_info if entry["folder"] not in deleted_subfolders] + logger.info('Removing deleted subfolders from metadata') + deleted_subfolders = [ + subfolder + for subfolder in [entry['folder'] for entry in self.meta_info if 'folder' in entry] + if subfolder not in subfolders + ] + self.meta_info = [entry for entry in self.meta_info if entry['folder'] not in deleted_subfolders] def recheck_validity_invalid_subfolders(self): - logger.info("Recheck invalid subfolders and update metadata") - invalid_subfolders = [entry["folder"] for entry in self.meta_info if not entry["valid"]] + logger.info('Recheck invalid subfolders and update metadata') + invalid_subfolders = [entry['folder'] for entry in self.meta_info if not entry['valid']] for subfolder in invalid_subfolders: self._remove_entry(subfolder) self._add_entry(subfolder) @@ -56,32 +64,36 @@ def _add_entry(self, subfolder): if os.path.isfile(os.path.join(subfolder_path, self.executable_name)): if self._is_valid(subfolder_path): version = self._get_version(subfolder_path) - self.meta_info.append({"id": rev_id, "folder": subfolder, "valid": True, "version": version}) + self.meta_info.append({'id': rev_id, 'folder': subfolder, 'valid': True, 'version': version}) else: - self.meta_info.append({"id": rev_id, "folder": subfolder, "valid": False}) + self.meta_info.append({'id': rev_id, 'folder': subfolder, 'valid': False}) else: - self.meta_info.append({"id": rev_id, "folder": subfolder, "valid": False}) + self.meta_info.append({'id': rev_id, 'folder': subfolder, 'valid': False}) def _remove_entry(self, subfolder: str): - self.meta_info = [entry for entry in self.meta_info if entry["folder"] != subfolder] + self.meta_info = [entry for entry in self.meta_info if entry['folder'] != subfolder] def _is_valid(self, build_path: str) -> bool: - command = "./%s --version" % self.executable_name + command = './%s --version' % self.executable_name return cli.execute_and_return_status(command, cwd=build_path) == 0 def _get_version(self, build_path: str) -> str: - command = "./%s --version" % self.executable_name + command = './%s --version' % self.executable_name return cli.execute_and_return_output(command, cwd=build_path) def _get_meta_info(self) -> dict: meta_file_path = os.path.join(self.builds_folder_path, META_FILE_NAME) - with open(meta_file_path, "r") as file: + with open(meta_file_path, 'r') as file: return json.load(file) def _overwrite_meta_info(self): meta_file_path = os.path.join(self.builds_folder_path, META_FILE_NAME) - with open(meta_file_path, "w") as file: + with open(meta_file_path, 'w') as file: json.dump(self.meta_info, file) def _get_subfolders(self) -> list: - return [subfolder for subfolder in os.listdir(self.builds_folder_path) if os.path.isdir(os.path.join(self.builds_folder_path, subfolder))] + return [ + subfolder + for subfolder in os.listdir(self.builds_folder_path) + if os.path.isdir(os.path.join(self.builds_folder_path, subfolder)) + ] diff --git a/bci/browser/binary/binary.py b/bughog/subject/webbrowser/binary/binary.py similarity index 81% rename from bci/browser/binary/binary.py rename to bughog/subject/webbrowser/binary/binary.py index 5e43591d..74e81637 100644 --- a/bci/browser/binary/binary.py +++ b/bughog/subject/webbrowser/binary/binary.py @@ -7,14 +7,14 @@ from typing import Optional from bci import util -from bci.browser.binary.artisanal_manager import ArtisanalBuildManager from bci.database.mongo.binary_cache import BinaryCache +from bci.subject.browser.binary.artisanal_manager import ArtisanalBuildManager from bci.version_control.states.state import State logger = logging.getLogger(__name__) -class Binary: +class BrowserBinary: def __init__(self, state: State): self.state = state self.__version = None @@ -64,30 +64,30 @@ def _list_downloaded_binaries(bin_folder_path: str) -> list[dict[str, str]]: @staticmethod def list_artisanal_binaries(bin_folder_path: str, executable_name: str): - return Binary._get_artisanal_manager(bin_folder_path, executable_name).get_artisanal_binaries_list() + return BrowserBinary._get_artisanal_manager(bin_folder_path, executable_name).get_artisanal_binaries_list() @staticmethod def _get_artisanal_manager(bin_folder_path: str, executable_name: str) -> ArtisanalBuildManager: return ArtisanalBuildManager(bin_folder_path, executable_name) - def fetch_binary(self): - # Check cache - if self.is_built(): - logger.info(f'Binary for {self.state.index} is already in place') - return - # Consult binary cache - elif BinaryCache.fetch_binary_files(self.get_potential_bin_path(), self.state): - logger.info(f'Binary for {self.state.index} fetched from cache') - return - # Try to download binary - elif self.is_available_online(): - start = time.time() - self.download_binary() - elapsed_time = time.time() - start - logger.info(f'Binary for {self.state.index} downloaded in {elapsed_time:.2f}s') - BinaryCache.store_binary_files(self.get_potential_bin_path(), self.state) - else: - raise BuildNotAvailableError(self.browser_name, self.state) + # def fetch_binary(self): + # # Check cache + # if self.is_built(): + # logger.info(f'Binary for {self.state.index} is already in place') + # return + # # Consult binary cache + # elif BinaryCache.fetch_binary_files(self.get_potential_bin_path(), self.state): + # logger.info(f'Binary for {self.state.index} fetched from cache') + # return + # # Try to download binary + # elif self.is_available_online(): + # start = time.time() + # self.download_binary() + # elapsed_time = time.time() - start + # logger.info(f'Binary for {self.state.index} downloaded in {elapsed_time:.2f}s') + # BinaryCache.store_binary_files(self.get_potential_bin_path(), self.state) + # else: + # raise BuildNotAvailableError(self.browser_name, self.state) def is_available(self): """ diff --git a/bci/browser/binary/factory.py b/bughog/subject/webbrowser/binary/factory.py similarity index 61% rename from bci/browser/binary/factory.py rename to bughog/subject/webbrowser/binary/factory.py index 736f24e8..edc4a7a7 100644 --- a/bci/browser/binary/factory.py +++ b/bughog/subject/webbrowser/binary/factory.py @@ -1,7 +1,7 @@ -from bci.browser.binary.binary import Binary -from bci.browser.binary.vendors.chromium import ChromiumBinary -from bci.browser.binary.vendors.firefox import FirefoxBinary -from bci.version_control.states.state import State +from bughog.browser.binary.binary import Binary +from bughog.browser.binary.vendors.chromium import ChromiumBinary +from bughog.browser.binary.vendors.firefox import FirefoxBinary +from bughog.version_control.states.base import State def get_binary(state: State) -> Binary: diff --git a/bughog/subject/webbrowser/binary/vendors/chromium.py b/bughog/subject/webbrowser/binary/vendors/chromium.py new file mode 100644 index 00000000..8241d79a --- /dev/null +++ b/bughog/subject/webbrowser/binary/vendors/chromium.py @@ -0,0 +1,69 @@ +import logging +import os +import re + +from bughog import cli, util +from bughog.database.mongo.executable_cache import ExecutableCache +from bughog.subject.webbrowser.binary.artisanal_manager import ArtisanalBuildManager +from bughog.subject.webbrowser.binary.binary import BrowserBinary +from bughog.version_control.states.base import State + +logger = logging.getLogger(__name__) + +EXECUTABLE_NAME = 'chrome' +BIN_FOLDER_PATH = '/app/browser/binaries/chromium' +EXTENSION_FOLDER_PATH = '/app/browser/extensions/chromium' + + +class ChromiumBinary(BrowserBinary): + def __init__(self, state: State): + super().__init__(state) + + # def save_browser_binary(self, binary_file): + # binary_file.save(self.get_bin_path()) + + # @property + # def executable_name(self) -> str: + # return EXECUTABLE_NAME + + # @property + # def browser_name(self) -> str: + # return 'chromium' + + # @property + # def bin_folder_path(self) -> str: + # return BIN_FOLDER_PATH + + # Downloadable binaries + + # def configure_binary(self): + # binary_folder = os.path.dirname(self.get_potential_bin_path()) + # self.__remove_unnecessary_files(binary_folder) + # cli.execute_and_return_status(f'chmod -R a+x {binary_folder}') + + # def __remove_unnecessary_files(self, binary_folder_path: str) -> None: + # """ + # Remove binary files that are not necessary for default usage of the browser. + # This is to improve performance, especially when caching binary files. + + # :param binary_folder_path: Path to the folder where the binary files are stored. + # """ + # locales_folder_path = os.path.join(binary_folder_path, 'locales') + # if os.path.isdir(locales_folder_path): + # util.remove_all_in_folder(locales_folder_path, except_files=['en-GB.pak', 'en-US.pak']) + + # def _get_version(self) -> str: + # command = './chrome --version' + # if bin_path := self.get_bin_path(): + # output = cli.execute_and_return_output(command, cwd=os.path.dirname(bin_path)) + # else: + # ExecutableCache.remove_executable_files(self.state) + # raise AttributeError(f'Could not get binary path for {self.state}') + # match = re.match(r'Chromium (?P[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)', output) + # if match: + # return match.group('version') + # raise AttributeError("Could not determine version of binary at '%s'. Version output: %s" % (bin_path, output)) + + @staticmethod + def get_artisanal_manager() -> ArtisanalBuildManager: + return BrowserBinary._get_artisanal_manager(BIN_FOLDER_PATH, EXECUTABLE_NAME) diff --git a/bughog/subject/webbrowser/binary/vendors/firefox.py b/bughog/subject/webbrowser/binary/vendors/firefox.py new file mode 100644 index 00000000..73ec683c --- /dev/null +++ b/bughog/subject/webbrowser/binary/vendors/firefox.py @@ -0,0 +1,58 @@ +import logging +import os +import re + +from bughog import cli +from bughog.subject.webbrowser.binary.artisanal_manager import ArtisanalBuildManager +from bughog.subject.webbrowser.binary.binary import BrowserBinary +from bughog.version_control.states.base import State + +logger = logging.getLogger('bci') + +EXECUTABLE_NAME = 'firefox' +BIN_FOLDER_PATH = '/app/browser/binaries/firefox' +EXTENSION_FOLDER_PATH = '/app/browser/extensions/firefox' + + +class FirefoxBinary(BrowserBinary): + def __init__(self, state: State): + super().__init__(state) + + # @property + # def executable_name(self) -> str: + # return 'firefox' + + # @property + # def browser_name(self) -> str: + # return 'firefox' + + # @property + # def bin_folder_path(self) -> str: + # return BIN_FOLDER_PATH + + # def configure_binary(self) -> None: + # binary_folder = os.path.dirname(self.get_potential_bin_path()) + # cli.execute_and_return_status(f'chmod -R a+x {binary_folder}') + # cli.execute_and_return_status(f'chmod -R a+w {binary_folder}') + # # Add policy.json to prevent updating. (this measure is effective from version 60) + # # https://github.com/mozilla/policy-templates/blob/master/README.md + # # (For earlier versions, the prefs.js file is used) + # distributions_path = os.path.join(binary_folder, 'distribution') + # os.makedirs(distributions_path, exist_ok=True) + # policies_path = os.path.join(distributions_path, 'policies.json') + # with open(policies_path, 'a') as file: + # file.write('{ "policies": { "DisableAppUpdate": true } }') + + # def _get_version(self): + # if (bin_path := self.get_bin_path()) is None: + # raise AttributeError(f'Binary not available for {self.browser_name} {self.state}') + # command = './firefox --version' + # output = cli.execute_and_return_output(command, cwd=os.path.dirname(bin_path)) + # match = re.match(r'Mozilla Firefox (?P[0-9]+)\.[0-9]+.*', output) + # if match: + # return match.group('version') + # raise AttributeError("Could not determine version of binary at '%s'. Version output: %s" % (bin_path, output)) + + @staticmethod + def get_artisanal_manager() -> ArtisanalBuildManager: + return BrowserBinary._get_artisanal_manager(BIN_FOLDER_PATH, EXECUTABLE_NAME) diff --git a/bci/evaluations/custom/__init__.py b/bughog/subject/webbrowser/chromium/__init__.py similarity index 100% rename from bci/evaluations/custom/__init__.py rename to bughog/subject/webbrowser/chromium/__init__.py diff --git a/bci/browser/configuration/chromium.py b/bughog/subject/webbrowser/chromium/executable.py similarity index 59% rename from bci/browser/configuration/chromium.py rename to bughog/subject/webbrowser/chromium/executable.py index 176b62a2..b4821753 100644 --- a/bci/browser/configuration/chromium.py +++ b/bughog/subject/webbrowser/chromium/executable.py @@ -1,14 +1,13 @@ -from bci.browser.configuration.browser import Browser -from bci.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing -from bci.browser.configuration.profile import prepare_chromium_profile +import os +import re -SUPPORTED_OPTIONS = [ - Default(), - BlockThirdPartyCookies(), - PrivateBrowsing() -] +from bughog import cli, util +from bughog.parameters import SubjectConfiguration +from bughog.subject.executable import Executable +from bughog.subject.webbrowser.profile import prepare_chromium_profile +from bughog.version_control.states.base import State -SELENIUM_USED_FLAGS = [ +DEFAULT_FLAGS = [ '--use-fake-ui-for-media-stream', '--ignore-certificate-errors', '--disable-background-networking', @@ -30,20 +29,40 @@ ] -class Chromium(Browser): +class ChromiumExecutable(Executable): + def __init__(self, config: SubjectConfiguration, state: State) -> None: + super().__init__(config, state) + + @property + def executable_name(self) -> str: + return 'chrome' + + def _get_version(self) -> str: + command = f'./{self.executable_name} --version' + output = cli.execute_and_return_output(command, cwd=self.executable_folder) + match = re.match(r'Chromium (?P[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)', output) + if match: + return match.group('version') + raise AttributeError(f"Could not determine version of binary at '{self.executable_name}'.") + + def _configure_executable(self): + # Remove unneccessary files + locales_folder_path = os.path.join(self.executable_folder, 'locales') + if os.path.isdir(locales_folder_path): + util.remove_all_in_folder(locales_folder_path, except_files=['en-GB.pak', 'en-US.pak']) + cli.execute_and_return_status(f'chmod -R a+x {self.executable_folder}') def get_navigation_sleep_duration(self) -> int: return 1 def get_open_console_hotkey(self) -> list[str]: - return ["ctrl", "shift", "j"] + return ['ctrl', 'shift', 'j'] def _get_terminal_args(self) -> list[str]: assert self._profile_path is not None - args = [self._get_executable_file_path()] + args = [self.executable_path] args.append(f'--user-data-dir={self._profile_path}') - # Enable logging args.append('--enable-logging') args.append('--v=1') args.append('--log-level=0') @@ -52,22 +71,19 @@ def _get_terminal_args(self) -> list[str]: # Also see: https://github.com/DistriNet/BugHog/issues/12 # args.append('--headless=new') # From Chrome - if 'btpc' in self.browser_config.browser_setting: + if 'btpc' in self.config.subject_setting: # This is handled in the profile folder pass - if 'pb' in self.browser_config.browser_setting: + if 'pb' in self.config.subject_setting: args.append('--incognito') - if self.browser_config.extensions: - raise AttributeError("Not implemented") - - args.extend(self.browser_config.cli_options) - args.extend(SELENIUM_USED_FLAGS) + args.extend(self.config.cli_options) + args.extend(DEFAULT_FLAGS) return args def _prepare_profile_folder(self): profile_path = None - match self.browser_config.browser_setting: + match self.config.subject_setting: case 'default': profile_path = prepare_chromium_profile() case 'btpc': @@ -86,5 +102,5 @@ def _prepare_profile_folder(self): elif int(self.version) < 86: profile_path = prepare_chromium_profile('59_btpc') else: - raise AttributeError("Chrome 86 and up not supported yet") + raise AttributeError('Chrome 86 and up not supported yet') self._profile_path = profile_path diff --git a/bughog/subject/webbrowser/chromium/repo.py b/bughog/subject/webbrowser/chromium/repo.py new file mode 100644 index 00000000..35993411 --- /dev/null +++ b/bughog/subject/webbrowser/chromium/repo.py @@ -0,0 +1,21 @@ +from bughog.subject.webbrowser.state_cache import PublicBrowserStateCache + + +def is_tag(tag: str) -> bool: + return PublicBrowserStateCache.is_tag('chromium', tag) + + +def get_release_tag(major_release_version: int) -> str: + return PublicBrowserStateCache.get_release_tag('chromium', major_release_version) + + +def get_release_revision_number(major_release_version: int) -> int: + return PublicBrowserStateCache.get_release_commit_number('chromium', major_release_version) + + +def get_release_revision_id(major_release_version: int) -> int: + return PublicBrowserStateCache.get_release_commit_id('chromium', major_release_version) + + +def get_most_recent_major_version() -> int: + return PublicBrowserStateCache.get_most_recent_major_version('chromium') diff --git a/bughog/subject/webbrowser/chromium/state_oracle.py b/bughog/subject/webbrowser/chromium/state_oracle.py new file mode 100644 index 00000000..03851cc3 --- /dev/null +++ b/bughog/subject/webbrowser/chromium/state_oracle.py @@ -0,0 +1,94 @@ +import logging +import re +from typing import Optional + +import requests + +from bughog import util +from bughog.database.mongo.cache import Cache +from bughog.subject.state_oracle import StateOracle +from bughog.subject.webbrowser.state_cache import PublicBrowserStateCache + +logger = logging.getLogger(__name__) + +REV_ID_BASE_URL = 'https://chromium.googlesource.com/chromium/src/+/' +REV_NUMBER_BASE_URL = 'http://crrev.com/' + + +class ChromiumStateOracle(StateOracle): + """ + Release state functions + """ + + @Cache.cache_in_db('webbrowser', 'chromium') + def has_publicly_available_release_executable(self, major_version: int) -> bool: + # TODO: check cache at factory + commit_nb = PublicBrowserStateCache.get_release_commit_number('chromium', major_version) + return self.has_publicly_available_commit_executable(commit_nb) + + def get_release_executable_download_urls(self, major_version: int) -> list[str]: + commit_nb = PublicBrowserStateCache.get_release_commit_number('chromium', major_version) + return self.get_commit_executable_download_urls(commit_nb) + + """ + Commit state functions + """ + + @Cache.cache_in_db('webbrowser', 'chromium') + def find_commit_nb(self, commit_id: str) -> int: + url = f'{REV_ID_BASE_URL}{commit_id}' + html = util.request_html(url).decode() + commit_nb = self.__parse_revision_number(html) + if commit_nb is None: + logging.getLogger('bci').error(f"Could not parse commit number on '{url}'") + raise AttributeError(f"Could not parse commit number on '{url}'") + assert re.match(r'[0-9]{1,7}', commit_nb) + return int(commit_nb) + + @Cache.cache_in_db('webbrowser', 'chromium') + def find_commit_id(self, commit_nb: int) -> str: + try: + final_url = util.request_final_url(f'{REV_NUMBER_BASE_URL}{commit_nb}') + except util.ResourceNotFound: + logger.warning(f"Could not find commit id for commit number '{commit_nb}'") + return None + rev_id = final_url[-40:] + assert re.match(r'[a-z0-9]{40}', rev_id) + return rev_id + + @Cache.cache_in_db('webbrowser', 'chromium') + def has_publicly_available_commit_executable(self, commit_nb: int) -> bool: + url = f'https://www.googleapis.com/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F{commit_nb}%2Fchrome-linux.zip' + req = requests.get(url) + has_binary_online = req.status_code == 200 + # TODO: caching at factory + return has_binary_online + + def get_commit_executable_download_urls(self, commit_nb: int) -> list[str]: + return [ + ( + 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/%s%%2F%s%%2Fchrome-%s.zip?alt=media' + % ('Linux_x64', commit_nb, 'linux') + ) + ] + + @Cache.cache_in_db('webbrowser', 'chromium') + def get_revision_nb(self, revision_id: str) -> int: + url = f'{REV_ID_BASE_URL}{revision_id}' + html = util.request_html(url).decode() + rev_number = self.__parse_revision_number(html) + if rev_number is None: + logging.getLogger('bci').error(f"Could not parse commit number on '{url}'") + raise AttributeError(f"Could not parse commit number on '{url}'") + assert re.match(r'[0-9]{1,7}', rev_number) + return int(rev_number) + + @staticmethod + def __parse_revision_number(html: str) -> Optional[str]: + matches = re.findall(r'refs\/heads\/(?:master|main)\@\{\#([0-9]{1,7})\}', html) + if matches: + return matches[0] + matches = re.findall(r'svn.chromium.org\/chrome\/trunk\/src\@([0-9]{1,7}) ', html) + if matches: + return matches[0] + return None diff --git a/bughog/subject/webbrowser/chromium/subject.py b/bughog/subject/webbrowser/chromium/subject.py new file mode 100644 index 00000000..c9968a5c --- /dev/null +++ b/bughog/subject/webbrowser/chromium/subject.py @@ -0,0 +1,76 @@ +import logging +import re +from typing import Optional + +from bughog.subject.state_oracle import StateOracle +from bughog.subject.webbrowser.chromium import repo +from bughog.subject.webbrowser.chromium.state_oracle import ChromiumStateOracle +from bughog.subject.webbrowser.base import Browser +from bughog.util import ResourceNotFound, request_final_url, request_html + +REV_ID_BASE_URL = 'https://chromium.googlesource.com/chromium/src/+/' +REV_NUMBER_BASE_URL = 'http://crrev.com/' + +logger = logging.getLogger(__name__) + + +class Chromium(Browser): + @staticmethod + def name() -> str: + return 'chromium' + + @staticmethod + def state_oracle_class() -> type[StateOracle]: + return ChromiumStateOracle + + @staticmethod + def get_availability() -> dict: + return {'name': 'chromium', 'min_version': 20, 'max_version': repo.get_most_recent_major_version()} + + # def has_online_binary(self) -> bool: + # cached_binary_available_online = MongoDB().has_executable_available_online(self) + # if cached_binary_available_online is not None: + # return cached_binary_available_online + # url = f'https://www.googleapis.com/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F{self._revision_nb}%2Fchrome-linux.zip' + # response = requests.get(url, stream=True) + # has_binary_online = response.status_code == 200 + # MongoDB().store_executable_availability_online_cache(self.type(), self.name(), has_binary_online has_binary_online) + # return has_binary_online + + def get_online_binary_urls(self) -> list[str]: + return [ + ( + 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/%s%%2F%s%%2Fchrome-%s.zip?alt=media' + % ('Linux_x64', self._revision_nb, 'linux') + ) + ] + + def get_revision_id(self, revision_nb: int) -> Optional[str]: + try: + final_url = request_final_url(f'{REV_NUMBER_BASE_URL}{revision_nb}') + except ResourceNotFound: + logger.warning(f"Could not find revision id for revision number '{revision_nb}'") + return None + rev_id = final_url[-40:] + assert re.match(r'[a-z0-9]{40}', rev_id) + return rev_id + + def get_revision_nb(self, revision_id: str) -> int: + url = f'{REV_ID_BASE_URL}{revision_id}' + html = request_html(url).decode() + rev_number = self.__parse_revision_number(html) + if rev_number is None: + logging.getLogger('bci').error(f"Could not parse revision number on '{url}'") + raise AttributeError(f"Could not parse revision number on '{url}'") + assert re.match(r'[0-9]{1,7}', rev_number) + return int(rev_number) + + @staticmethod + def __parse_revision_number(html: str) -> Optional[str]: + matches = re.findall(r'refs\/heads\/(?:master|main)\@\{\#([0-9]{1,7})\}', html) + if matches: + return matches[0] + matches = re.findall(r'svn.chromium.org\/chrome\/trunk\/src\@([0-9]{1,7}) ', html) + if matches: + return matches[0] + return None diff --git a/bci/browser/cli_options/chromium.py b/bughog/subject/webbrowser/cli_options/chromium.py similarity index 100% rename from bci/browser/cli_options/chromium.py rename to bughog/subject/webbrowser/cli_options/chromium.py diff --git a/bci/browser/cli_options/firefox.py b/bughog/subject/webbrowser/cli_options/firefox.py similarity index 100% rename from bci/browser/cli_options/firefox.py rename to bughog/subject/webbrowser/cli_options/firefox.py diff --git a/bci/integration_tests/__init__.py b/bughog/subject/webbrowser/configuration/__init__.py similarity index 100% rename from bci/integration_tests/__init__.py rename to bughog/subject/webbrowser/configuration/__init__.py diff --git a/bci/browser/configuration/browser.py b/bughog/subject/webbrowser/configuration/browser.py similarity index 61% rename from bci/browser/configuration/browser.py rename to bughog/subject/webbrowser/configuration/browser.py index 0c38d69a..b1eea37d 100644 --- a/bci/browser/configuration/browser.py +++ b/bughog/subject/webbrowser/configuration/browser.py @@ -4,13 +4,13 @@ import subprocess from abc import abstractmethod -import bci.browser.binary.factory as binary_factory -from bci import util -from bci.browser.automation.terminal import TerminalAutomation -from bci.browser.binary.binary import Binary -from bci.browser.configuration.profile import remove_profile_execution_folder -from bci.evaluations.logic import BrowserConfiguration, EvaluationConfiguration -from bci.version_control.states.state import State +import bughog.browser.binary.factory as binary_factory +from bughog import util +from bughog.browser.automation.terminal import TerminalAutomation +from bughog.browser.binary.binary import Binary +from bughog.browser.configuration.profile import remove_profile_execution_folder +from bughog.parameters import EvaluationConfiguration, SubjectConfiguration +from bughog.version_control.states.base import State EXECUTION_PARENT_FOLDER = '/tmp' @@ -19,11 +19,10 @@ class Browser: process: subprocess.Popen | None def __init__( - self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary + self, browser_config: SubjectConfiguration, binary: Binary ) -> None: self.browser_config = browser_config self.process = None - self.eval_config = eval_config self.binary = binary self.state = binary.state self._profile_path = None @@ -55,35 +54,6 @@ def terminate(self): TerminalAutomation.terminate_browser(self.process, self._get_terminal_args()) self.process = None - def pre_evaluation_setup(self): - self.__fetch_binary() - - def post_evaluation_cleanup(self): - self.__remove_binary() - - def pre_test_setup(self): - self.__prepare_execution_folder() - - def post_test_cleanup(self): - self.__remove_execution_folder() - - def pre_try_setup(self): - self._prepare_profile_folder() - - def post_try_cleanup(self): - self.__remove_profile_folder() - self.__empty_downloads_folder() - - def __fetch_binary(self): - self.binary.fetch_binary() - - def __remove_binary(self): - self.binary.remove_bin_folder() - - def __prepare_execution_folder(self): - path = self.__get_execution_folder_path() - util.copy_folder(self.binary.get_bin_folder_path(), path) - @abstractmethod def _prepare_profile_folder(self): pass @@ -121,16 +91,16 @@ def get_open_console_hotkey(self) -> list[str]: @staticmethod def get_browser( - browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State + browser_config: SubjectConfiguration, eval_config: EvaluationConfiguration, state: State ) -> Browser: - from bci.browser.configuration.chromium import Chromium - from bci.browser.configuration.firefox import Firefox + from bughog.browser.configuration.chromium import Chromium + from bughog.browser.configuration.firefox import Firefox binary = binary_factory.get_binary(state) - if browser_config.browser_name == 'chromium': + if browser_config.subject_name == 'chromium': return Chromium(browser_config, eval_config, binary) - elif browser_config.browser_name == 'firefox': + elif browser_config.subject_name == 'firefox': return Firefox(browser_config, eval_config, binary) else: raise AttributeError('Not implemented') diff --git a/bughog/subject/webbrowser/configuration/chromium.py b/bughog/subject/webbrowser/configuration/chromium.py new file mode 100644 index 00000000..9391db80 --- /dev/null +++ b/bughog/subject/webbrowser/configuration/chromium.py @@ -0,0 +1,90 @@ +# from bughog.subject.browser.configuration.browser import Browser +# from bughog.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing +# from bughog.browser.configuration.profile import prepare_chromium_profile + +# SUPPORTED_OPTIONS = [ +# Default(), +# BlockThirdPartyCookies(), +# PrivateBrowsing() +# ] + +# SELENIUM_USED_FLAGS = [ +# '--use-fake-ui-for-media-stream', +# '--ignore-certificate-errors', +# '--disable-background-networking', +# '--disable-client-side-phishing-detection', +# '--disable-component-update', +# '--disable-default-apps', +# '--disable-gpu', +# '--disable-hang-monitor', +# '--disable-popup-blocking', +# '--disable-prompt-on-repost', +# '--disable-sync', +# '--disable-web-resources', +# '--metrics-recording-only', +# '--no-first-run', +# '--password-store=basic', +# '--safebrowsing-disable-auto-update', +# '--use-mock-keychain', +# '--no-sandbox', +# ] + + +# class Chromium(Browser): + +# def get_navigation_sleep_duration(self) -> int: +# return 1 + +# def get_open_console_hotkey(self) -> list[str]: +# return ["ctrl", "shift", "j"] + +# def _get_terminal_args(self) -> list[str]: +# assert self._profile_path is not None + +# args = [self._get_executable_file_path()] +# args.append(f'--user-data-dir={self._profile_path}') +# # Enable logging +# args.append('--enable-logging') +# args.append('--v=1') +# args.append('--log-level=0') +# # Headless changed from version +/- 110 onwards: https://developer.chrome.com/docs/chromium/new-headless +# # Using the `--headless` flag will crash the browser for these later versions. +# # Also see: https://github.com/DistriNet/BugHog/issues/12 +# # args.append('--headless=new') # From Chrome + +# if 'btpc' in self.browser_config.browser_setting: +# # This is handled in the profile folder +# pass +# if 'pb' in self.browser_config.browser_setting: +# args.append('--incognito') + +# if self.browser_config.extensions: +# raise AttributeError("Not implemented") + +# args.extend(self.browser_config.cli_options) +# args.extend(SELENIUM_USED_FLAGS) +# return args + +# def _prepare_profile_folder(self): +# profile_path = None +# match self.browser_config.browser_setting: +# case 'default': +# profile_path = prepare_chromium_profile() +# case 'btpc': +# if int(self.version) < 17: +# profile_path = prepare_chromium_profile('6_btpc') +# elif int(self.version) < 24: +# profile_path = prepare_chromium_profile('17_btpc') +# elif int(self.version) < 36: +# profile_path = prepare_chromium_profile('24_btpc') +# elif int(self.version) < 40: +# profile_path = prepare_chromium_profile('36_btpc') +# elif int(self.version) < 46: +# profile_path = prepare_chromium_profile('40_btpc') +# elif int(self.version) < 59: +# profile_path = prepare_chromium_profile('46_btpc') +# elif int(self.version) < 86: +# profile_path = prepare_chromium_profile('59_btpc') +# else: +# raise AttributeError("Chrome 86 and up not supported yet") +# self._profile_path = profile_path diff --git a/bci/browser/configuration/firefox.py b/bughog/subject/webbrowser/configuration/firefox.py similarity index 94% rename from bci/browser/configuration/firefox.py rename to bughog/subject/webbrowser/configuration/firefox.py index d543ce33..691432b1 100644 --- a/bci/browser/configuration/firefox.py +++ b/bughog/subject/webbrowser/configuration/firefox.py @@ -1,9 +1,9 @@ import os -from bci import cli -from bci.browser.configuration.browser import Browser -from bci.browser.configuration.options import BlockThirdPartyCookies, Default, PrivateBrowsing, TrackingProtection -from bci.browser.configuration.profile import prepare_firefox_profile +from bughog import cli +from bughog.browser.configuration.browser import Browser +from bughog.browser.configuration.options import BlockThirdPartyCookies, Default, PrivateBrowsing, TrackingProtection +from bughog.browser.configuration.profile import prepare_firefox_profile SUPPORTED_OPTIONS = [ Default(), diff --git a/bci/browser/configuration/options.py b/bughog/subject/webbrowser/configuration/options.py similarity index 100% rename from bci/browser/configuration/options.py rename to bughog/subject/webbrowser/configuration/options.py diff --git a/bughog/subject/webbrowser/evaluation.py b/bughog/subject/webbrowser/evaluation.py new file mode 100644 index 00000000..b0a8f3c9 --- /dev/null +++ b/bughog/subject/webbrowser/evaluation.py @@ -0,0 +1,25 @@ +from bughog.subject.evaluation_framework import EvaluationFramework + + +class BrowserEvaluationFramework(EvaluationFramework): + def experiment_is_valid(self, project: str, experiment: str) -> bool: + dir_tree = self.evaluation_framework.dir_tree + # Always runnable if there is either an interaction script or url_queue present + if 'script' in data or 'url_queue' in data: + return True + + # Should have exactly one main folder otherwise + domains = dir_tree[project][experiment] + main_paths = [paths for paths in domains.values() if paths is not None and 'main' in paths.keys()] + if len(main_paths) != 1: + return False + # Main should have index.html + if 'index.html' not in main_paths[0]['main'].keys(): + return False + return True + + def create_empty_experiment(self, project: str, experiment: str): + pass + + def get_default_file_content(self, file_type: str): + pass diff --git a/bughog/subject/webbrowser/executable.py b/bughog/subject/webbrowser/executable.py new file mode 100644 index 00000000..b13d7a7d --- /dev/null +++ b/bughog/subject/webbrowser/executable.py @@ -0,0 +1,31 @@ +from abc import abstractmethod +from bughog import util +from bughog.parameters import SubjectConfiguration +from bughog.subject.executable import Executable +from bughog.version_control.states.base import State + + +class BrowserExecutable(Executable): + PROFILE_STORAGE_FOLDER = '/app/subject/webbrowser/profiles' + PROFILE_EXECUTION_FOLDER = '/tmp/profiles' + + def __init__(self, config: SubjectConfiguration, state: State) -> None: + super().__init__(config, state) + + @abstractmethod + def get_navigation_sleep_duration(self) -> int: + pass + + @abstractmethod + def get_open_console_hotkey(self) -> list[str]: + pass + + def __fetch_binary(self): + self.binary.fetch_binary() + + def __remove_binary(self): + self.binary.remove_bin_folder() + + def __prepare_execution_folder(self): + path = self.__get_execution_folder_path() + util.copy_folder(self.binary.get_bin_folder_path(), path) diff --git a/bci/search_strategy/__init__.py b/bughog/subject/webbrowser/firefox/__init__.py similarity index 100% rename from bci/search_strategy/__init__.py rename to bughog/subject/webbrowser/firefox/__init__.py diff --git a/bughog/subject/webbrowser/firefox/executable.py b/bughog/subject/webbrowser/firefox/executable.py new file mode 100644 index 00000000..a74e7dfa --- /dev/null +++ b/bughog/subject/webbrowser/firefox/executable.py @@ -0,0 +1,37 @@ +import os +import re + +from bughog import cli +from bughog.parameters import SubjectConfiguration +from bughog.subject.webbrowser.executable import BrowserExecutable +from bughog.version_control.states.base import State + + +class FirefoxExecutable(BrowserExecutable): + def __init__(self, config: SubjectConfiguration, state: State) -> None: + super().__init__(config, state) + + @property + def executable_name(self) -> str: + return 'firefox' + + def _get_version(self): + command = './firefox --version' + output = cli.execute_and_return_output(command, cwd=self.executable_folder) + match = re.match(r'Mozilla Firefox (?P[0-9]+)\.[0-9]+.*', output) + if match: + return match.group('version') + raise AttributeError(f"Could not determine version of binary at '{self.executable_name}'.") + + def _configure_executable(self): + binary_folder = self.executable_folder + cli.execute_and_return_status(f'chmod -R a+x {binary_folder}') + cli.execute_and_return_status(f'chmod -R a+w {binary_folder}') + # Add policy.json to prevent updating. (this measure is effective from version 60) + # https://github.com/mozilla/policy-templates/blob/master/README.md + # (For earlier versions, the prefs.js file is used) + distributions_path = os.path.join(binary_folder, 'distribution') + os.makedirs(distributions_path, exist_ok=True) + policies_path = os.path.join(distributions_path, 'policies.json') + with open(policies_path, 'a') as file: + file.write('{ "policies": { "DisableAppUpdate": true } }') diff --git a/bughog/subject/webbrowser/firefox/repo.py b/bughog/subject/webbrowser/firefox/repo.py new file mode 100644 index 00000000..464e4904 --- /dev/null +++ b/bughog/subject/webbrowser/firefox/repo.py @@ -0,0 +1,21 @@ +from bughog.subject.webbrowser.state_cache import PublicBrowserStateCache + + +def is_tag(tag: str) -> bool: + return PublicBrowserStateCache.is_tag('firefox', tag) + + +def get_release_tag(major_release_version: int) -> str: + return PublicBrowserStateCache.get_release_tag('firefox', major_release_version) + + +def get_release_revision_number(major_release_version: int) -> int: + return PublicBrowserStateCache.get_release_commit_number('firefox', major_release_version) + + +def get_release_revision_id(major_release_version: int) -> int: + return PublicBrowserStateCache.get_release_commit_id('firefox', major_release_version) + + +def get_most_recent_major_version() -> int: + return PublicBrowserStateCache.get_most_recent_major_version('firefox') diff --git a/bughog/subject/webbrowser/firefox/state_oracle.py b/bughog/subject/webbrowser/firefox/state_oracle.py new file mode 100644 index 00000000..bcb94827 --- /dev/null +++ b/bughog/subject/webbrowser/firefox/state_oracle.py @@ -0,0 +1,40 @@ +from bughog.database.mongo.cache import Cache +from bughog.subject.state_oracle import StateOracle +from bughog.subject.webbrowser.state_cache import PublicBrowserStateCache + + +class FirefoxStateOracle(StateOracle): + + @Cache.cache_in_db('webbrowser', 'firefox') + def find_commit_nb(self, commit_id: str) -> int: + return PublicBrowserStateCache.firefox_get_commit_number(commit_id) + + @Cache.cache_in_db('webbrowser', 'firefox') + def find_commit_id(self, commit_nb: int) -> str: + return PublicBrowserStateCache.firefox_get_commit_id(commit_nb) + + @Cache.cache_in_db('webbrowser', 'firefox') + def has_publicly_available_release_executable(self, major_version: int) -> bool: + return True + + def get_release_executable_download_urls(self, major_version: int) -> list[str]: + return [ + f'https://ftp.mozilla.org/pub/firefox/releases/{major_version}.0/linux-x86_64/en-US/firefox-{major_version}.0.tar.bz2', + f'https://ftp.mozilla.org/pub/firefox/releases/{major_version}.0/linux-x86_64/en-US/firefox-{major_version}.0.tar.xz', + ] + + @Cache.cache_in_db('webbrowser', 'firefox') + def has_publicly_available_commit_executable(self, commit_nb: int) -> bool: + return PublicBrowserStateCache.firefox_has_executable_for(commit_nb=commit_nb) + + def get_commit_executable_download_urls(self, commit_nb: int) -> list[str]: + commit_id = self.find_commit_id(commit_nb) + result = PublicBrowserStateCache.firefox_get_executable_info(commit_id) + if result is None: + raise AttributeError(f"Could not find binary url for '{commit_nb}'") + binary_base_url = result['files_url'] + app_version = result['app_version'] + return [ + f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.bz2', + f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.xz', + ] diff --git a/bughog/subject/webbrowser/firefox/subject.py b/bughog/subject/webbrowser/firefox/subject.py new file mode 100644 index 00000000..c42fc75b --- /dev/null +++ b/bughog/subject/webbrowser/firefox/subject.py @@ -0,0 +1,22 @@ +from bughog.subject.state_oracle import StateOracle +from bughog.subject.webbrowser.base import Browser +from bughog.subject.webbrowser.firefox import repo +from bughog.subject.webbrowser.firefox.state_oracle import FirefoxStateOracle + + +class Firefox(Browser): + @staticmethod + def name() -> str: + return 'firefox' + + @staticmethod + def state_oracle() -> type[StateOracle]: + return FirefoxStateOracle + + @staticmethod + def get_availability() -> dict: + return { + 'name': 'firefox', + 'min_version': 20, + 'max_version': repo.get_most_recent_major_version(), + } diff --git a/bci/version_control/__init__.py b/bughog/subject/webbrowser/interaction/__init__.py similarity index 100% rename from bci/version_control/__init__.py rename to bughog/subject/webbrowser/interaction/__init__.py diff --git a/bci/browser/interaction/elements/five.png b/bughog/subject/webbrowser/interaction/elements/five.png similarity index 100% rename from bci/browser/interaction/elements/five.png rename to bughog/subject/webbrowser/interaction/elements/five.png diff --git a/bci/browser/interaction/elements/four.png b/bughog/subject/webbrowser/interaction/elements/four.png similarity index 100% rename from bci/browser/interaction/elements/four.png rename to bughog/subject/webbrowser/interaction/elements/four.png diff --git a/bci/browser/interaction/elements/one.png b/bughog/subject/webbrowser/interaction/elements/one.png similarity index 100% rename from bci/browser/interaction/elements/one.png rename to bughog/subject/webbrowser/interaction/elements/one.png diff --git a/bci/browser/interaction/elements/six.png b/bughog/subject/webbrowser/interaction/elements/six.png similarity index 100% rename from bci/browser/interaction/elements/six.png rename to bughog/subject/webbrowser/interaction/elements/six.png diff --git a/bci/browser/interaction/elements/three.png b/bughog/subject/webbrowser/interaction/elements/three.png similarity index 100% rename from bci/browser/interaction/elements/three.png rename to bughog/subject/webbrowser/interaction/elements/three.png diff --git a/bci/browser/interaction/elements/two.png b/bughog/subject/webbrowser/interaction/elements/two.png similarity index 100% rename from bci/browser/interaction/elements/two.png rename to bughog/subject/webbrowser/interaction/elements/two.png diff --git a/bci/browser/interaction/interaction.py b/bughog/subject/webbrowser/interaction/interaction.py similarity index 64% rename from bci/browser/interaction/interaction.py rename to bughog/subject/webbrowser/interaction/interaction.py index a277db20..69c6f464 100644 --- a/bci/browser/interaction/interaction.py +++ b/bughog/subject/webbrowser/interaction/interaction.py @@ -2,33 +2,32 @@ from inspect import signature from urllib.parse import quote_plus -from bci.browser.configuration.browser import Browser as BrowserConfig -from bci.browser.interaction.simulation import Simulation -from bci.evaluations.logic import TestParameters -from bci.browser.interaction.simulation_exception import SimulationException +from bughog.subject.interaction import Interaction +from bughog.subject.webbrowser.executable import BrowserExecutable +from bughog.subject.webbrowser.interaction.simulation import Simulation +from bughog.subject.webbrowser.interaction.simulation_exception import SimulationException logger = logging.getLogger(__name__) -class Interaction: - browser: BrowserConfig - script: list[str] - params: TestParameters +class BrowserInteraction(Interaction): - def __init__(self, browser: BrowserConfig, script: list[str], params: TestParameters) -> None: - self.browser = browser - self.script = script - self.params = params + def __init__(self, executable: BrowserExecutable, script: list[str], params: ExperimentParameters) -> None: + super().__init__(executable, script, params) - def execute(self) -> None: - simulation = Simulation(self.browser, self.params) + def _do_experiment(self) -> None: + simulation = Simulation(self.executable, self.params) + if self._interpret(simulation): + simulation.sleep(str(self.executable.get_navigation_sleep_duration())) + def _do_sanity_check(self) -> None: + simulation = Simulation(self.executable, self.params) if self._interpret(simulation): - simulation.sleep(str(self.browser.get_navigation_sleep_duration())) simulation.navigate('https://a.test/report/?bughog_sanity_check=OK') + def _interpret(self, simulation: Simulation) -> bool: - try: + try: for statement in self.script: if statement.strip() == '' or statement[0] == '#': continue @@ -61,5 +60,7 @@ def _interpret(self, simulation: Simulation) -> bool: return True except Exception as e: # Unexpected exception type - not sane, report the exception - simulation.navigate(f'https://a.test/report/?uncaught-exception={quote_plus(type(e).__name__)}&message={quote_plus(str(e))}') - return False \ No newline at end of file + simulation.navigate( + f'https://a.test/report/?uncaught-exception={quote_plus(type(e).__name__)}&message={quote_plus(str(e))}' + ) + return False diff --git a/bci/browser/interaction/simulation.py b/bughog/subject/webbrowser/interaction/simulation.py similarity index 77% rename from bci/browser/interaction/simulation.py rename to bughog/subject/webbrowser/interaction/simulation.py index 9f799cc8..4cf02309 100644 --- a/bci/browser/interaction/simulation.py +++ b/bughog/subject/webbrowser/interaction/simulation.py @@ -5,14 +5,15 @@ import Xlib.display from pyvirtualdisplay.display import Display -from bci.browser.configuration.browser import Browser as BrowserConfig -from bci.browser.interaction.simulation_exception import SimulationException -from bci.evaluations.logic import TestParameters +from bughog.parameters import ExperimentParameters +from bughog.subject.webbrowser.executable import BrowserExecutable +from bughog.subject.webbrowser.interaction.simulation_exception import SimulationException + class Simulation: - browser_config: BrowserConfig - params: TestParameters + executable: BrowserExecutable + params: ExperimentParameters public_methods: list[str] = [ 'navigate', @@ -32,15 +33,15 @@ class Simulation: 'open_console', ] - def __init__(self, browser_config: BrowserConfig, params: TestParameters): - self.browser_config = browser_config + def __init__(self, browser_config: BrowserExecutable, params: ExperimentParameters): + self.executable = browser_config self.params = params disp = Display(visible=True, size=(1920, 1080), backend='xvfb', use_xauth=True) disp.start() gui._pyautogui_x11._display = Xlib.display.Display(os.environ['DISPLAY']) def __del__(self): - self.browser_config.terminate() + self.executable.terminate() def parse_position(self, position: str, max_value: int) -> int: # Screen percentage @@ -52,9 +53,9 @@ def parse_position(self, position: str, max_value: int) -> int: # --- PUBLIC METHODS --- def navigate(self, url: str): - self.browser_config.terminate() - self.browser_config.open(url) - self.sleep(str(self.browser_config.get_navigation_sleep_duration())) + self.executable.terminate() + self.executable.open(url) + self.sleep(str(self.executable.get_navigation_sleep_duration())) self.click_position("100", "50%") # focus the browser window def new_tab(self, url: str): @@ -62,7 +63,7 @@ def new_tab(self, url: str): self.sleep("0.5") self.write(url) self.press("enter") - self.sleep(str(self.browser_config.get_navigation_sleep_duration())) + self.sleep(str(self.executable.get_navigation_sleep_duration())) def click_position(self, x: str, y: str): max_x, max_y = gui.size() @@ -94,12 +95,12 @@ def sleep(self, duration: str): sleep(float(duration)) def screenshot(self, filename: str): - filename = f'{self.params.evaluation_configuration.project}-{self.params.mech_group}-{filename}-{type(self.browser_config).__name__}-{self.browser_config.version}.jpg' + filename = f'{self.params.evaluation_configuration.project}-{self.params.experiment}-{filename}-{type(self.executable).__name__}-{self.executable.version}.jpg' filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../../logs/screenshots', filename) gui.screenshot(filepath) def report_leak(self): - self.navigate(f'https://a.test/report/?leak={self.params.mech_group}') + self.navigate(f'https://a.test/report/?leak={self.params.experiment}') def assert_file_contains(self, filename: str, content: str): filepath = os.path.join('/root/Downloads', filename) @@ -110,10 +111,10 @@ def assert_file_contains(self, filename: str, content: str): with open(filepath, 'r') as f: if content not in f.read(): raise SimulationException(f'file-{filename}-does-not-contain-{content}') - + def open_file(self, filename: str): self.navigate(f'file:///root/Downloads/{filename}') def open_console(self): - self.hotkey(*self.browser_config.get_open_console_hotkey()) + self.hotkey(*self.executable.get_open_console_hotkey()) self.sleep("1.5") diff --git a/bci/browser/interaction/simulation_exception.py b/bughog/subject/webbrowser/interaction/simulation_exception.py similarity index 100% rename from bci/browser/interaction/simulation_exception.py rename to bughog/subject/webbrowser/interaction/simulation_exception.py diff --git a/bci/browser/configuration/profile.py b/bughog/subject/webbrowser/profile.py similarity index 96% rename from bci/browser/configuration/profile.py rename to bughog/subject/webbrowser/profile.py index 79e03d12..b07c0669 100644 --- a/bci/browser/configuration/profile.py +++ b/bughog/subject/webbrowser/profile.py @@ -1,9 +1,9 @@ import os from typing import Optional -from bci import cli +from bughog import cli -PROFILE_STORAGE_FOLDER = '/app/browser/profiles' +PROFILE_STORAGE_FOLDER = '/app/subject/browser/profiles' PROFILE_EXECUTION_FOLDER = '/tmp/profiles' diff --git a/bci/version_control/repository/repository.py b/bughog/subject/webbrowser/repository.py similarity index 100% rename from bci/version_control/repository/repository.py rename to bughog/subject/webbrowser/repository.py diff --git a/bughog/subject/webbrowser/state_cache.py b/bughog/subject/webbrowser/state_cache.py new file mode 100644 index 00000000..1746b1ff --- /dev/null +++ b/bughog/subject/webbrowser/state_cache.py @@ -0,0 +1,136 @@ +import logging +from concurrent.futures import ThreadPoolExecutor +from typing import Optional + +from pymongo import ASCENDING, DESCENDING + +from bughog import util +from bughog.database.mongo.mongodb import MongoDB + +logger = logging.getLogger(__name__) + +BASE_URL = 'https://bughog.distrinet-research.be/' + + +class PublicBrowserStateCache: + @staticmethod + def update() -> None: + def safe_request_json_and_update(collection_name: str, transform=lambda x: x): + url = BASE_URL + collection_name + '.json' + try: + result = util.request_json(url)['data'] + if result is not None: + PublicBrowserStateCache.__update_collection(collection_name, transform(result)) + except util.ResourceNotFound: + logger.warning(f'Could not update commit cache with resource at {url}') + except Exception: + logger.error(f'Could not update commit cache for {collection_name}', exc_info=True) + + executor = ThreadPoolExecutor() + executor.submit( + safe_request_json_and_update, 'firefox_executable_availability', transform=lambda x: list(x.values()) + ) + executor.submit(safe_request_json_and_update, 'firefox_release_base_revs') + executor.submit(safe_request_json_and_update, 'chromium_release_base_revs') + executor.shutdown(wait=False) + + @staticmethod + def __update_collection(collection_name: str, data: list) -> None: + collection = MongoDB().get_collection(collection_name) + if (n := len(data)) == collection.count_documents({}): + logger.debug(f'{collection_name} is still up-to-date ({n} documents).') + else: + collection.delete_many({}) + collection.insert_many(data) + logger.info(f'{collection_name} is updated ({len(data)} documents).') + + @staticmethod + def firefox_get_commit_number(commit_id: str) -> int: + collection = MongoDB().get_collection('firefox_executable_availability') + result = collection.find_one({'commit_id': commit_id}, {'commit_number': 1}) + if result is None or 'commit_number' not in result: + raise AttributeError(f"Could not find 'commit_number' in {result}") + return result['commit_number'] + + @staticmethod + def firefox_has_executable_for(commit_nb: Optional[int]=None, commit_id: Optional[str]=None) -> bool: + collection = MongoDB().get_collection('firefox_executable_availability') + if commit_nb: + result = collection.find_one({'commit_number': commit_nb}) + elif commit_id: + result = collection.find_one({'commit_number': commit_nb}) + else: + raise AttributeError('No commit number or id was provided') + return result is not None + + @staticmethod + def firefox_get_executable_info(commit_id: str) -> Optional[dict]: + collection = MongoDB().get_collection('firefox_executable_availability') + return collection.find_one({'node': commit_id}, {'files_url': 1, 'app_version': 1}) + + @staticmethod + def firefox_get_previous_and_next_commit_nb_with_executable(commit_nb: int) -> tuple[Optional[int], Optional[int]]: + collection = MongoDB().get_collection('firefox_executable_availability') + + previous_commit_nbs = collection.find({'commit_number': {'$lt': commit_nb}}).sort({'commit_number': DESCENDING}) + previous_document = next(previous_commit_nbs, None) + + next_commit_nbs = collection.find({'commit_number': {'$gt': commit_nb}}).sort({'commit_number': ASCENDING}) + next_document = next(next_commit_nbs, None) + + return ( + previous_document['commit_number'] if previous_document else None, + next_document['commit_number'] if next_document else None, + ) + + @staticmethod + def firefox_get_commit_id(commit_nb: int) -> Optional[str]: + collection = MongoDB().get_collection('firefox_executable_availability') + result = collection.find_one({'commit_number': commit_nb}) + if result is None: + return None + return result.get('node', None) + + @staticmethod + def __get_release_base_rev_collection(browser: str) -> str: + match browser: + case 'chromium': + return 'chromium_release_base_revs' + case 'firefox': + return 'firefox_release_base_revs' + case _: + raise AttributeError(f'Could not get collection for browser {browser}') + + @staticmethod + def is_tag(browser: str, tag: str) -> bool: + collection = MongoDB().get_collection(PublicBrowserStateCache.__get_release_base_rev_collection(browser)) + n = collection.count_documents({'release_tag': tag}) + return n > 0 + + @staticmethod + def get_release_tag(browser: str, major_release_version: int) -> str: + collection = MongoDB().get_collection(PublicBrowserStateCache.__get_release_base_rev_collection(browser)) + if doc := collection.find_one({'major_version': major_release_version}): + return doc['release_tag'] + raise AttributeError(f"Could not find release tag associated with version '{major_release_version}'") + + @staticmethod + def get_release_commit_number(browser: str, major_release_version: int) -> int: + collection = MongoDB().get_collection(PublicBrowserStateCache.__get_release_base_rev_collection(browser)) + if doc := collection.find_one({'major_version': major_release_version}): + return doc['commit_number'] + raise AttributeError(f"Could not find major release version '{major_release_version}'") + + @staticmethod + def get_release_commit_id(browser: str, major_release_version: int) -> int: + collection = MongoDB().get_collection(PublicBrowserStateCache.__get_release_base_rev_collection(browser)) + if doc := collection.find_one({'major_version': major_release_version}): + return doc['commit_id'] + raise AttributeError(f"Could not find major release version '{major_release_version}'") + + @staticmethod + def get_most_recent_major_version(browser: str) -> int: + collection = MongoDB().get_collection(PublicBrowserStateCache.__get_release_base_rev_collection(browser)) + if doc := collection.find_one(sort=[('major_version', -1)]): + return doc['major_version'] + raise AttributeError('Could not find most recent major release version') diff --git a/bci/util.py b/bughog/util.py similarity index 100% rename from bci/util.py rename to bughog/util.py diff --git a/bci/version_control/repository/__init__.py b/bughog/version_control/__init__.py similarity index 100% rename from bci/version_control/repository/__init__.py rename to bughog/version_control/__init__.py diff --git a/bci/version_control/revision_parser/chromium_parser.py b/bughog/version_control/revision_parser/chromium_parser.py similarity index 91% rename from bci/version_control/revision_parser/chromium_parser.py rename to bughog/version_control/revision_parser/chromium_parser.py index f8210e7b..3edb00eb 100644 --- a/bci/version_control/revision_parser/chromium_parser.py +++ b/bughog/version_control/revision_parser/chromium_parser.py @@ -2,8 +2,8 @@ import re from typing import Optional -from bci.util import ResourceNotFound, request_final_url, request_html -from bci.version_control.revision_parser.parser import RevisionParser +from bughog.util import ResourceNotFound, request_final_url, request_html +from bughog.version_control.revision_parser.parser import RevisionParser REV_ID_BASE_URL = 'https://chromium.googlesource.com/chromium/src/+/' REV_NUMBER_BASE_URL = 'http://crrev.com/' diff --git a/bci/version_control/revision_parser/parser.py b/bughog/version_control/revision_parser/parser.py similarity index 100% rename from bci/version_control/revision_parser/parser.py rename to bughog/version_control/revision_parser/parser.py diff --git a/bughog/version_control/state/base.py b/bughog/version_control/state/base.py new file mode 100644 index 00000000..22129739 --- /dev/null +++ b/bughog/version_control/state/base.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import base64 +import pickle +from abc import abstractmethod +from dataclasses import dataclass +from typing import Optional + +from bughog.subject.state_oracle import StateOracle + + +@dataclass(frozen=True) +class StateResult: + requests: list[dict[str, str]] + logs: list[str] + result_vars: list[dict[str, str]] + is_dirty: bool + reproduced: bool + + def has_same_outcome(self, other: StateResult) -> bool: + """ + Returns whether this and the given other result share the same outcome. + + :returns bool: True if both state results are reproduced, not reproduced, or are both dirty. + """ + return self.is_dirty == other.is_dirty and self.reproduced == other.reproduced + + def from_dict(result: dict) -> StateResult: + return StateResult( + result['requests'], + result + ) + + def __repr__(self) -> str: + return f'StateResult(reproduced={self.reproduced}, dirty={self.is_dirty})' + + +class State: + def __init__(self, oracle: StateOracle): + self.oracle = oracle + self.result_variables: Optional[State] + + # def has_dirty_result(self) -> bool: + # """ + # Returns whether this state has a dirty result. + + # :returns bool: True if this state has a result, which is dirty. + # """ + # return self.result is not None and self.result.is_dirty + + # def has_dirty_or_no_result(self) -> bool: + # """ + # Returns whether this state has no result or a dirty result. + + # :returns bool: True if this state has no result, or a dirty result. + # """ + # return self.result is None or self.result.is_dirty + + # def has_same_outcome(self, other: State) -> bool: + # """ + # Returns whether this and the given other state share the same result outcome. + + # :returns bool: True if states are both reproduced, not reproduced, or dirty. + # """ + # if self.result is None or other.result is None: + # return False + # else: + # return self.result.has_same_outcome(other.result) + + @property + @abstractmethod + def name(self) -> str: + pass + + @property + @abstractmethod + def type(self) -> str: + pass + + @property + @abstractmethod + def index(self) -> int: + """ + The index of the element in the sequence. + """ + pass + + @property + @abstractmethod + def commit_nb(self) -> int: + pass + + @abstractmethod + def to_parameters(self) -> StateParameters: + pass + + @classmethod + def from_parameters(cls, oracle: StateOracle, params: StateParameters) -> State: + from bughog.version_control.states.revisions.base import CommitState + from bughog.version_control.states.versions.base import ReleaseState + + if params.type == 'release': + return ReleaseState(oracle, params.version_or_commit) + elif params.type == 'commit': + return CommitState(oracle, commit_nb=params.version_or_commit) + else: + raise ValueError('Unknown state type') + + def serialize(self) -> str: + """ + Returns a dictionary representation of the state. + """ + pickled_bytes = pickle.dumps(self, pickle.HIGHEST_PROTOCOL) + return base64.b64encode(pickled_bytes).decode('ascii') + + @staticmethod + def deserialize(pickled_str: str) -> State: + pickled_bytes = base64.b64decode(pickled_str) + return pickle.loads(pickled_bytes) + + @abstractmethod + def to_dict(self) -> dict: + pass + + @staticmethod + def from_dict(subject_type: str, subject_name: str, data: dict) -> State: + from bughog.subject import factory + + subject_class = factory.get_subject_class(subject_type, subject_name) + oracle = subject_class.state_oracle + match data['type']: + case 'commit': + state_class = subject_class.commit_state_class + return state_class(oracle, commit_nb=data.get('commit_nb'), commit_id=data.get('commit_id')) + case 'release': + state_class = subject_class.release_state_class + return state_class(oracle, release_version=data['release_version']) + case _: + raise Exception(f'Unknown state type: {data["type"]}') + + @abstractmethod + def has_executable_online(self) -> bool: + pass + + @abstractmethod + def has_publicly_available_executable(self) -> bool: + pass + + @abstractmethod + def get_executable_source_urls(self) -> list[str]: + """ + Returns a list of URLs where the associated binary can potentially be downloaded from. + """ + pass + + def get_previous_and_next_state_with_binary(self) -> tuple[State, State]: + raise NotImplementedError(f'This function is not implemented for {self}') + + def __repr__(self) -> str: + return f'State(index={self.index})' + + def __eq__(self, other: object) -> bool: + if not isinstance(other, State): + return False + return self.index == other.index + + def __hash__(self) -> int: + return hash((self.index, self.subject.name)) diff --git a/bughog/version_control/state/commit/base.py b/bughog/version_control/state/commit/base.py new file mode 100644 index 00000000..449f34b7 --- /dev/null +++ b/bughog/version_control/state/commit/base.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import logging +import re +from typing import Optional + +from bughog.subject import factory +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.state.base import State + +logger = logging.getLogger(__name__) + + +class CommitState(State): + def __init__(self, oracle: StateOracle, commit_id: Optional[str] = None, commit_nb: Optional[int] = None): + super().__init__(oracle) + if commit_id is None and commit_nb is None: + raise AttributeError('A state must be initiliazed with either a revision id or revision number') + + self._commit_id = commit_id + self._commit_nb = commit_nb + self._fetch_missing_data() + + if self._commit_id is not None and not self.oracle.is_valid_commit_id(self._commit_id): + raise AttributeError(f"Invalid revision id '{self._commit_id}' for state '{self}'") + + if self._commit_nb is not None and not self.oracle.is_valid_commit_nb(self._commit_nb): + raise AttributeError(f"Invalid revision number '{self._commit_nb}' for state '{self}'") + + @property + def name(self) -> str: + return f'{self._commit_nb}' + + @property + def type(self) -> str: + return 'revision' + + @property + def index(self) -> int: + return self._commit_nb + + @property + def commit_nb(self) -> int: + return self._commit_nb + + def _has_revision_id(self) -> bool: + return self._commit_id is not None + + def _has_revision_number(self) -> bool: + return self._commit_nb is not None + + def _fetch_missing_data(self) -> None: + """ + States are initialized with either a revision id or revision number. + This method attempts to fetch other data to complete this state object. + """ + # TODO: solve with oracle + # # First check if the missing data is available in the cache + # if self._commit_id and self._commit_nb: + # return + # if state := MongoDB().get_complete_state_dict_from_binary_availability_cache(self): + # if self._commit_id is None: + # self._commit_id = state.get('revision_id', None) + # if self._commit_nb is None: + # self._commit_nb = state.get('revision_number', None) + # # If not, fetch the missing data from the parser + # if self._commit_id is None: + # self._commit_id = self.oracle.find_commit_id(self.commit_nb) + # if self._commit_nb is None: + # self._commit_nb = self.oracle.find_commit_nb(self._commit_id) + + def _is_valid_commit_id(self, revision_id: str) -> bool: + """ + Checks if a revision id is valid. + A valid revision id is a 40 character long string containing only lowercase letters and numbers. + """ + return re.match(r'[a-z0-9]{40}', revision_id) is not None + + def _is_valid_commit_number(self, revision_number: int) -> bool: + """ + Checks if a revision number is valid. + A valid revision number is a positive integer. + """ + return re.match(r'[0-9]{1,7}', str(revision_number)) is not None + + def has_publicly_available_executable(self) -> bool: + return self.oracle.has_publicly_available_commit_executable(self.commit_nb) + + def get_executable_source_urls(self) -> list[str]: + return self.oracle.get_commit_executable_download_urls(self.commit_nb) + + def __str__(self): + return f'CommitState(number: {self._commit_nb}, id: {self._commit_id})' + + def __repr__(self): + return f'CommitState(number: {self._commit_nb}, id: {self._commit_id})' diff --git a/bughog/version_control/state/commit/chromium.py b/bughog/version_control/state/commit/chromium.py new file mode 100644 index 00000000..cbc9cabe --- /dev/null +++ b/bughog/version_control/state/commit/chromium.py @@ -0,0 +1,18 @@ +# from typing import Optional + +# import requests + +# from bughog.database.mongo.mongodb import MongoDB +# from bughog.version_control.revision_parser.chromium_parser import ChromiumRevisionParser +# from bughog.version_control.states.revisions.base import CommitState + +# PARSER = ChromiumRevisionParser() + + +# class ChromiumRevision(CommitState): +# def __init__(self, revision_id: Optional[str] = None, revision_nb: Optional[int] = None): +# super().__init__(revision_id, revision_nb) + +# @property +# def browser_name(self): +# return 'chromium' diff --git a/bughog/version_control/state/commit/firefox.py b/bughog/version_control/state/commit/firefox.py new file mode 100644 index 00000000..d8af8e94 --- /dev/null +++ b/bughog/version_control/state/commit/firefox.py @@ -0,0 +1,47 @@ +# from typing import Optional + +# from bughog.database.mongo.revision_cache import RevisionCache +# from bughog.version_control.states.base import State +# from bughog.version_control.states.revisions.base import CommitState + + +# class FirefoxRevision(CommitState): +# def __init__( +# self, revision_id: Optional[str] = None, revision_nb: Optional[int] = None, major_version: Optional[int] = None +# ): +# super().__init__(revision_id=revision_id, revision_nb=revision_nb) +# self.major_version = major_version + +# @property +# def browser_name(self) -> str: +# return 'firefox' + +# def has_online_binary(self) -> bool: +# return RevisionCache.firefox_has_binary_for(revision_nb=self.revision_nb, revision_id=self._revision_id) + +# def get_online_binary_urls(self) -> list[str]: +# result = RevisionCache.firefox_get_binary_info(self._revision_id) +# if result is None: +# raise AttributeError(f"Could not find binary url for '{self._revision_id}'") +# binary_base_url = result['files_url'] +# app_version = result['app_version'] +# return [ +# f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.bz2', +# f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.xz', +# ] + +# def get_previous_and_next_state_with_binary(self) -> tuple[State, State]: +# previous_revision_nb, next_revision_nb = RevisionCache.firefox_get_previous_and_next_revision_nb_with_binary( +# self.revision_nb +# ) + +# return ( +# FirefoxRevision(revision_nb=previous_revision_nb) if previous_revision_nb else None, +# FirefoxRevision(revision_nb=next_revision_nb) if next_revision_nb else None, +# ) + +# def _fetch_missing_data(self): +# if self._revision_id is None: +# self._revision_id = RevisionCache.firefox_get_revision_id(self._revision_nb) +# if self._revision_nb is None and self._revision_id is not None: +# self._revision_nb = RevisionCache.firefox_get_revision_number(self._revision_id) diff --git a/bci/version_control/states/versions/base.py b/bughog/version_control/state/release/base.py similarity index 69% rename from bci/version_control/states/versions/base.py rename to bughog/version_control/state/release/base.py index 9c58639a..f8ae6f7f 100644 --- a/bci/version_control/states/versions/base.py +++ b/bughog/version_control/state/release/base.py @@ -1,11 +1,12 @@ from abc import abstractmethod -from bci.version_control.states.state import State +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.state.base import State -class BaseVersion(State): - def __init__(self, major_version: int): - super().__init__() +class ReleaseState(State): + def __init__(self, oracle: StateOracle, major_version: int): + super().__init__(oracle) self.major_version = major_version self._revision_nb = self._get_rev_nb() self._revision_id = self._get_rev_id() @@ -22,11 +23,6 @@ def _get_rev_id(self) -> str: def name(self) -> str: return f'v_{self.major_version}' - @property - @abstractmethod - def browser_name(self) -> str: - pass - @property def type(self) -> str: return 'version' @@ -36,7 +32,7 @@ def index(self) -> int: return self.major_version @property - def revision_nb(self) -> int: + def commit_nb(self) -> int: return self._revision_nb def to_dict(self, make_complete: bool = True) -> dict: @@ -50,8 +46,8 @@ def to_dict(self, make_complete: bool = True) -> dict: @staticmethod def from_dict(data: dict) -> State: - from bci.version_control.states.versions.chromium import ChromiumVersion - from bci.version_control.states.versions.firefox import FirefoxVersion + from bughog.version_control.states.versions.chromium import ChromiumVersion + from bughog.version_control.states.versions.firefox import FirefoxVersion match data['browser_name']: case 'chromium': @@ -66,6 +62,12 @@ def from_dict(data: dict) -> State: def convert_to_revision(self) -> State: pass + def has_publicly_available_executable(self) -> bool: + return self.oracle.has_publicly_available_release_executable(self.major_version) + + def get_executable_source_urls(self) -> list[str]: + return self.oracle.get_release_executable_download_urls(self.major_version) + def __str__(self): return f'VersionState(version: {self.major_version}, rev: {self._revision_nb})' diff --git a/bughog/version_control/state/release/chromium.py b/bughog/version_control/state/release/chromium.py new file mode 100644 index 00000000..168cb4eb --- /dev/null +++ b/bughog/version_control/state/release/chromium.py @@ -0,0 +1,42 @@ +# import requests + +# from bughog.database.mongo.mongodb import MongoDB +# from bughog.version_control.repository.online.chromium import get_release_revision_id, get_release_revision_number +# from bughog.version_control.states.revisions.chromium import ChromiumRevision +# from bughog.version_control.states.versions.base import ReleaseState + + +# class ChromiumVersion(ReleaseState): +# def __init__(self, major_version: int): +# super().__init__(major_version) + +# def _get_rev_nb(self) -> int: +# return get_release_revision_number(self.major_version) + +# def _get_rev_id(self) -> str: +# return get_release_revision_id(self.major_version) + +# @property +# def browser_name(self): +# return 'chromium' + +# def has_online_binary(self): +# cached_binary_available_online = MongoDB().has_executable_available_online('chromium', self) +# if cached_binary_available_online is not None: +# return cached_binary_available_online +# url = f'https://www.googleapis.com/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F{self._revision_nb}%2Fchrome-linux.zip' +# req = requests.get(url) +# has_binary_online = req.status_code == 200 +# MongoDB().store_binary_availability_online_cache('chromium', self, has_binary_online) +# return has_binary_online + +# def get_online_binary_urls(self) -> list[str]: +# return [ +# ( +# 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/%s%%2F%s%%2Fchrome-%s.zip?alt=media' +# % ('Linux_x64', self._revision_nb, 'linux') +# ) +# ] + +# def convert_to_revision(self) -> ChromiumRevision: +# return ChromiumRevision(revision_nb=self._revision_nb) diff --git a/bughog/version_control/state/release/firefox.py b/bughog/version_control/state/release/firefox.py new file mode 100644 index 00000000..58ea4821 --- /dev/null +++ b/bughog/version_control/state/release/firefox.py @@ -0,0 +1,30 @@ +# from bughog.version_control.repository.online.firefox import get_release_revision_id, get_release_revision_number +# from bughog.version_control.states.revisions.firefox import FirefoxRevision +# from bughog.version_control.states.versions.base import ReleaseState + + +# class FirefoxVersion(ReleaseState): +# def __init__(self, major_version: int): +# super().__init__(major_version) + +# def _get_rev_nb(self) -> int: +# return get_release_revision_number(self.major_version) + +# def _get_rev_id(self) -> str: +# return get_release_revision_id(self.major_version) + +# @property +# def browser_name(self) -> str: +# return 'firefox' + +# def has_online_binary(self) -> bool: +# return True + +# def get_online_binary_urls(self) -> list[str]: +# return [ +# f'https://ftp.mozilla.org/pub/firefox/releases/{self.major_version}.0/linux-x86_64/en-US/firefox-{self.major_version}.0.tar.bz2', +# f'https://ftp.mozilla.org/pub/firefox/releases/{self.major_version}.0/linux-x86_64/en-US/firefox-{self.major_version}.0.tar.xz', +# ] + +# def convert_to_revision(self) -> FirefoxRevision: +# return FirefoxRevision(revision_nb=self._revision_nb) diff --git a/bci/version_control/state_factory.py b/bughog/version_control/state_factory.py similarity index 57% rename from bci/version_control/state_factory.py rename to bughog/version_control/state_factory.py index f5ad7a92..354cfdbd 100644 --- a/bci/version_control/state_factory.py +++ b/bughog/version_control/state_factory.py @@ -1,25 +1,22 @@ from __future__ import annotations -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import EvaluationParameters -from bci.version_control.state_result_factory import StateResultFactory -from bci.version_control.states.revisions.chromium import ChromiumRevision -from bci.version_control.states.revisions.firefox import FirefoxRevision -from bci.version_control.states.state import State -from bci.version_control.states.versions.base import BaseVersion -from bci.version_control.states.versions.chromium import ChromiumVersion -from bci.version_control.states.versions.firefox import FirefoxVersion +from bughog.database.mongo.mongodb import MongoDB +from bughog.parameters import EvaluationParameters +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.state.base import State +from bughog.version_control.state.commit.base import CommitState +from bughog.version_control.state.release.base import ReleaseState class StateFactory: - def __init__(self, eval_params: EvaluationParameters) -> None: + def __init__(self, state_oracle: StateOracle, eval_params: EvaluationParameters) -> None: """ Create a state factory object with the given evaluation parameters and boundary indices. :param eval_params: The evaluation parameters. """ + self.__oracle = state_oracle self.__eval_params = eval_params - self.__state_result_factory = StateResultFactory(experiment=eval_params.evaluation_range.mech_group) self.boundary_states = self.__create_boundary_states() def create_state(self, index: int) -> State: @@ -46,8 +43,8 @@ def __create_boundary_states(self) -> tuple[State, State]: first_state = self.__create_version_state(eval_range.major_version_range[0]) last_state = self.__create_version_state(eval_range.major_version_range[1]) if not eval_range.only_release_revisions: - first_state = first_state.convert_to_revision() - last_state = last_state.convert_to_revision() + first_state = first_state.convert_to_commit_state() + last_state = last_state.convert_to_commit_state() return first_state, last_state elif eval_range.revision_number_range: if eval_range.only_release_revisions: @@ -65,28 +62,14 @@ def create_evaluated_states(self) -> list[State]: """ return MongoDB().get_evaluated_states(self.__eval_params, self.boundary_states, self.__state_result_factory) - def __create_version_state(self, index: int) -> BaseVersion: + def __create_version_state(self, index: int) -> ReleaseState: """ Create a version state object associated with the given index. """ - browser_config = self.__eval_params.browser_configuration - match browser_config.browser_name: - case 'chromium': - return ChromiumVersion(index) - case 'firefox': - return FirefoxVersion(index) - case _: - raise ValueError(f'Unknown browser name: {browser_config.browser_name}') + return self.__subject.release_state_class(self.__oracle, index) - def __create_revision_state(self, index: int) -> State: + def __create_revision_state(self, index: int) -> CommitState: """ Create a revision state object associated with the given index. """ - browser_config = self.__eval_params.browser_configuration - match browser_config.browser_name: - case 'chromium': - return ChromiumRevision(revision_nb=index) - case 'firefox': - return FirefoxRevision(revision_nb=index) - case _: - raise ValueError(f'Unknown browser name: {browser_config.browser_name}') + return self.__subject.commit_state_class(self.__oracle, index) diff --git a/bci/version_control/repository/online/__init__.py b/bughog/web/__init__.py similarity index 100% rename from bci/version_control/repository/online/__init__.py rename to bughog/web/__init__.py diff --git a/bughog/web/blueprints/api.py b/bughog/web/blueprints/api.py new file mode 100644 index 00000000..5446a263 --- /dev/null +++ b/bughog/web/blueprints/api.py @@ -0,0 +1,280 @@ +import json +import logging +import os +import threading + +from flask import Blueprint, current_app, request + +import bughog.parameters as application_logic +from bughog.analysis.plot_factory import PlotFactory +from bughog.app import sock +from bughog.configuration import Global, Loggers +from bughog.main import Main +from bughog.parameters import MissingParametersException +from bughog.subject import factory +from bughog.subject.factory import get_subject_availability +from bughog.web.clients import Clients + +logger = logging.getLogger(__name__) +api = Blueprint('api', __name__, url_prefix='/api') + +THREAD = None + + +def __start_thread(func, args=None): + global THREAD + if args is None: + args = [] + if THREAD and THREAD.is_alive(): + raise AttributeError() + else: + THREAD = threading.Thread(target=func, args=args) + THREAD.start() + + +def __get_main() -> Main: + if main := current_app.config['main']: + return main + raise Exception('Main object is not instantiated') + + +@api.before_request +def check_readiness(): + try: + pass + # _ = ____get_main() + except Exception as e: + logger.critical(e) + return {'status': 'NOK', 'msg': 'BugHog is not ready', 'info': {'log': Loggers.get_logs()}} + + +@api.after_request +def add_headers(response): + if 'DEVELOPMENT' in os.environ and os.environ['DEVELOPMENT'] == '1': + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Headers'] = 'Content-Type' + response.headers['Access-Control-Allow-Methods'] = '*' + return response + + +""" +Starting and stopping processses +""" + + +@api.route('/evaluation/start/', methods=['POST']) +def start_evaluation(): + if request.json is None: + return {'status': 'NOK', 'msg': 'No evaluation parameters found'} + + data = request.json.copy() + try: + params = application_logic.evaluation_factory(data) + __start_thread(__get_main().run, args=[params]) + return {'status': 'OK'} + except MissingParametersException: + return {'status': 'NOK', 'msg': 'Could not start evaluation due to missing parameters'} + except AttributeError: + return {'status': 'NOK', 'msg': 'Evaluation thread is already running'} + + +@api.route('/evaluation/stop/', methods=['POST']) +def stop_evaluation(): + if request.json is None: + return {'status': 'NOK', 'msg': 'No stop parameters found'} + + data = request.json.copy() + forcefully = data.get('forcefully', False) + if forcefully: + __get_main().activate_stop_forcefully() + else: + __get_main().activate_stop_gracefully() + return {'status': 'OK'} + + +""" +Requesting information +""" + + +@sock.route('/socket/', bp=api) +def init_websocket(ws): + logger.info('Client connected') + Clients.add_client(ws) + while True: + message = ws.receive() + if message is None: + break + try: + message = json.loads(message) + if params := message.get('new_browser', None): + Clients.associate_browser(ws, params) + if params := message.get('new_params', None): + Clients.associate_params(ws, params) + if params := message.get('select_project', None): + Clients.associate_project(ws, params) + if requested_variables := message.get('get', []): + __get_main().push_info(ws, *requested_variables) + except ValueError: + logger.warning('Ignoring invalid message from client.') + ws.send('Connected to BugHog') + + +@api.route('/subject/', methods=['GET']) +def get_subjects(): + return {'status': 'OK', 'subject_availability': get_subject_availability()} + + +@api.route('/system/', methods=['GET']) +def get_system_info(): + return {'status': 'OK', 'cpu_count': os.cpu_count() if os.cpu_count() else 2} + + +@api.route('/log/', methods=['POST']) +def log(): + # TODO: emit logs of workers in central log + return {'status': 'OK'} + + +@api.route('/data/', methods=['PUT']) +def data_source(): + if request.json is None: + return {'status': 'NOK', 'msg': 'No data parameters found'} + + params = request.json.copy() + plot_params = PlotParameters.from_dict(params) + if missing_params := PlotFactory.validate_params(plot_params): + return {'status': 'NOK', 'msg': f'Missing plot parameters: {missing_params}'} + return { + 'status': 'OK', + 'revision': PlotFactory.get_plot_commit_data(params), + 'version': PlotFactory.get_plot_release_data(params), + } + + +@api.route('/poc//', methods=['GET']) +def get_projects(subject_type: str): + return {'status': 'OK', 'projects': factory.create_experiments(subject_type).get_projects()} + + +@api.route('/poc//', methods=['POST']) +def create_project(subject_type: str): + if request.json is None: + return {'status': 'NOK', 'msg': 'No parameters found'} + project_name = request.json.get('project_name') + try: + factory.create_experiments(subject_type).create_empty_project(project_name) + return {'status': 'OK'} + except AttributeError as e: + return {'status': 'NOK', 'msg': str(e)} + + +@api.route('/poc///', methods=['GET']) +def get_experiments(subject_type: str, project: str): + experiments = factory.create_experiments(subject_type).get_experiments(project) + return {'status': 'OK', 'experiments': experiments} + + +@api.route('/poc////', methods=['GET']) +def poc(subject_type: str, project: str, poc: str): + return {'status': 'OK', 'tree': factory.create_experiments(subject_type).get_poc_structure(project, poc)} + + +@api.route('/poc/////', methods=['GET', 'POST']) +def poc_file_content(subject_type: str, project: str, poc: str, file: str): + domain = request.args.get('domain', '') + path = request.args.get('path', '') + if request.method == 'GET': + return { + 'status': 'OK', + 'content': factory.create_experiments(subject_type).get_poc_file(project, poc, domain, path, file), + } + else: + if not request.json: + return {'status': 'NOK', 'msg': 'No content to update file with'} + data = request.json.copy() + content = data['content'] + success = factory.create_experiments(subject_type).update_poc_file(project, poc, domain, path, file, content) + if success: + return {'status': 'OK'} + else: + return {'status': 'NOK'} + +@api.route('/poc////', methods=['POST']) +def add_page(subject_type: str, project: str, poc: str): + if request.json is None: + return {'status': 'NOK', 'msg': 'No page parameters found'} + + data = request.json.copy() + domain = data['domain'] + path = data['page'] + file_type = data['file_type'] + try: + factory.create_experiments(subject_type).add_page(project, poc, domain, path, file_type) + return {'status': 'OK'} + except AttributeError as e: + return {'status': 'NOK', 'msg': str(e)} + + +@api.route('/poc////config', methods=['POST']) +def add_config(subject_type: str, project: str, poc: str): + if request.json is None: + return {'status': 'NOK', 'msg': 'No parameters found'} + data = request.json.copy() + type = data['type'] + success = factory.create_experiments(subject_type).add_config(project, poc, type) + if success: + return {'status': 'OK'} + else: + return {'status': 'NOK'} + + +@api.route('/poc/domain/', methods=['GET']) +def get_available_domains(): + return {'status': 'OK', 'domains': Global.get_available_domains()} + + +@api.route('/poc///', methods=['POST']) +def create_experiment(subject_type: str, project: str): + if request.json is None: + return {'status': 'NOK', 'msg': 'No experiment parameters found'} + + data = request.json.copy() + if 'poc_name' not in data.keys(): + return {'status': 'NOK', 'msg': 'Missing experiment name'} + poc_name = data['poc_name'] + try: + factory.create_experiments(subject_type).create_empty_poc(project, poc_name) + return {'status': 'OK'} + except AttributeError as e: + return {'status': 'NOK', 'msg': str(e)} + + +# @api.route('/data/remove/', methods=['POST']) +# def remove_datapoint(): +# if (params := application_logic.TestParameters.from_dict(request.json)) is None: +# return {'status': 'NOK', 'msg': 'No parameters found'} +# __get_main().remove_datapoint(params) +# return {'status': 'OK'} + + +# @api.route('/test/start/', methods=['POST']) +# def integration_tests_start(): +# # Remove all previous data +# MongoDB().remove_all_data_from_collection('integrationtests_chromium') +# MongoDB().remove_all_data_from_collection('integrationtests_firefox') +# # Start integration tests +# all_experiments = __get_main().evaluation_framework.get_experiments('IntegrationTests') +# elegible_experiments = [experiment[0] for experiment in all_experiments if experiment[1]] +# eval_parameters_list = get_eval_parameters_list(elegible_experiments) +# __start_thread(__get_main().run, args=[eval_parameters_list]) +# return redirect('/test/') + + +# @api.route('/test/continue/', methods=['POST']) +# def integration_tests_continue(): +# all_experiments = __get_main().evaluation_framework.get_experiments('IntegrationTests') +# elegible_experiments = [experiment[0] for experiment in all_experiments if experiment[1]] +# eval_parameters_list = get_eval_parameters_list(elegible_experiments) +# __start_thread(__get_main().run, args=[eval_parameters_list]) +# return redirect('/test/') diff --git a/bci/web/blueprints/experiments.py b/bughog/web/blueprints/experiments.py similarity index 98% rename from bci/web/blueprints/experiments.py rename to bughog/web/blueprints/experiments.py index d4609204..4c80f1c6 100644 --- a/bci/web/blueprints/experiments.py +++ b/bughog/web/blueprints/experiments.py @@ -5,7 +5,7 @@ import threading import requests -from bci.evaluations.experiments import SUPPORTED_DOMAINS +from bughog.evaluation.experiment import SUPPORTED_DOMAINS from flask import Blueprint, Request, make_response, render_template, request, url_for logger = logging.getLogger(__name__) diff --git a/bci/web/blueprints/test.py b/bughog/web/blueprints/test.py similarity index 71% rename from bci/web/blueprints/test.py rename to bughog/web/blueprints/test.py index 0b2dc3e7..f32e7d56 100644 --- a/bci/web/blueprints/test.py +++ b/bughog/web/blueprints/test.py @@ -2,9 +2,9 @@ from flask import Blueprint, current_app, render_template -from bci.integration_tests.evaluation_configurations import get_eval_parameters_list -from bci.integration_tests.verify_results import verify -from bci.main import Main +from bughog.integration_tests.evaluation_configurations import get_eval_parameters_list +from bughog.integration_tests.verify_results import verify +from bughog.main import Main logger = logging.getLogger(__name__) test = Blueprint('test', __name__, url_prefix='/test') @@ -18,7 +18,7 @@ def __get_main() -> Main: @test.route('/') def index(): - all_experiments = __get_main().evaluation_framework.get_mech_groups('IntegrationTests') + all_experiments = __get_main().evaluation_framework.get_experiments('IntegrationTests') elegible_experiments = [experiment[0] for experiment in all_experiments if experiment[1]] eval_parameters_list = get_eval_parameters_list(elegible_experiments) verification_results = verify(eval_parameters_list) diff --git a/bci/web/clients.py b/bughog/web/clients.py similarity index 82% rename from bci/web/clients.py rename to bughog/web/clients.py index 5aa56da9..d7842401 100644 --- a/bci/web/clients.py +++ b/bughog/web/clients.py @@ -5,9 +5,9 @@ from flask import current_app from simple_websocket import Server -from bci.analysis.plot_factory import PlotFactory -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import PlotParameters +from bughog.analysis.plot_factory import PlotFactory +from bughog.database.mongo.mongodb import MongoDB +from bughog.parameters import EvaluationParameters class Clients: @@ -53,14 +53,15 @@ def associate_project(ws_client: Server, project: str): @staticmethod def push_results(ws_client: Server): if params := Clients.__clients.get(ws_client, None): - plot_params = PlotParameters.from_dict(params) + # plot_params = PlotParameters.from_dict(params) + plot_params = EvaluationParameters.from_dict(params) if PlotFactory.validate_params(plot_params): revision_data = None version_data = None else: - revision_data = PlotFactory.get_plot_revision_data(plot_params) - version_data = PlotFactory.get_plot_version_data(plot_params) + revision_data = PlotFactory.get_plot_commit_data(plot_params) + version_data = PlotFactory.get_plot_release_data(plot_params) ws_client.send( json.dumps( @@ -98,11 +99,13 @@ def push_experiments(ws_client: Server): logger.error('Could not find any associated info for this client') return - project = client_info.get('project', None) + subject_type = client_info.get('subject_type') + project = client_info.get('project') if project: - from bci.main import Main + from bughog.main import Main + main: Main = current_app.config['main'] - experiments = main.evaluation_framework.get_mech_groups(project) + experiments = main.get_experiments(subject_type, project) ws_client.send(json.dumps({'update': {'experiments': experiments}})) @staticmethod @@ -115,12 +118,4 @@ def push_experiments_to_all(): def push_previous_cli_options(ws_client: Server): if params := Clients.__clients.get(ws_client, None): previous_cli_options = MongoDB().get_previous_cli_options(params) - ws_client.send( - json.dumps( - { - 'update': { - 'previous_cli_options': previous_cli_options - } - } - ) - ) + ws_client.send(json.dumps({'update': {'previous_cli_options': previous_cli_options}})) diff --git a/bci/web/templates/base.html b/bughog/web/templates/base.html similarity index 100% rename from bci/web/templates/base.html rename to bughog/web/templates/base.html diff --git a/bci/web/templates/cookies.html b/bughog/web/templates/cookies.html similarity index 100% rename from bci/web/templates/cookies.html rename to bughog/web/templates/cookies.html diff --git a/bci/web/templates/experiment.html b/bughog/web/templates/experiment.html similarity index 100% rename from bci/web/templates/experiment.html rename to bughog/web/templates/experiment.html diff --git a/bci/web/templates/integration_tests.html b/bughog/web/templates/integration_tests.html similarity index 100% rename from bci/web/templates/integration_tests.html rename to bughog/web/templates/integration_tests.html diff --git a/bci/web/vue/.gitignore b/bughog/web/vue/.gitignore similarity index 100% rename from bci/web/vue/.gitignore rename to bughog/web/vue/.gitignore diff --git a/bci/web/vue/index.html b/bughog/web/vue/index.html similarity index 100% rename from bci/web/vue/index.html rename to bughog/web/vue/index.html diff --git a/bci/web/vue/package-lock.json b/bughog/web/vue/package-lock.json similarity index 100% rename from bci/web/vue/package-lock.json rename to bughog/web/vue/package-lock.json diff --git a/bci/web/vue/package.json b/bughog/web/vue/package.json similarity index 100% rename from bci/web/vue/package.json rename to bughog/web/vue/package.json diff --git a/bci/web/vue/postcss.config.js b/bughog/web/vue/postcss.config.js similarity index 100% rename from bci/web/vue/postcss.config.js rename to bughog/web/vue/postcss.config.js diff --git a/bci/web/vue/public/.gitkeep b/bughog/web/vue/public/.gitkeep similarity index 100% rename from bci/web/vue/public/.gitkeep rename to bughog/web/vue/public/.gitkeep diff --git a/bci/web/vue/src/App.vue b/bughog/web/vue/src/App.vue similarity index 90% rename from bci/web/vue/src/App.vue rename to bughog/web/vue/src/App.vue index 21507e40..d84504a8 100644 --- a/bci/web/vue/src/App.vue +++ b/bughog/web/vue/src/App.vue @@ -21,16 +21,16 @@ export default { return { timer: null, projects: [], - browsers: [], - browser_settings: [], + subject_availability: [], + subject_settings: [], cli_options_str: "", previous_cli_options_list: [], db_collection_suffix: "", tests: [], select_all_tests: false, curr_options: { - min_browser_version: 0, - max_browser_version: 100 + min_subject_version: 0, + max_subject_version: 100 }, slider: { state: [0, 100], @@ -38,9 +38,10 @@ export default { }, eval_params: { check_for: "request", - // Browser config - browser_name: null, - browser_setting: "default", + // Subject config + subject_type: 'webbrowser', + subject_name: null, + subject_setting: "default", cli_options: [], extensions: [], // Eval config @@ -90,7 +91,7 @@ export default { target_mech_id: null, fatal_error: null, hide_advanced_evaluation_options: true, - hide_advanced_browser_options: true, + hide_advanced_subject_options: true, hide_logs: true, hide_poc_editor: true, system: null, @@ -101,7 +102,7 @@ export default { computed: { "missing_plot_params": function () { const missing_params = []; - const required_params_for_plotting = ["browser_name", "project", "plot_mech_group"]; + const required_params_for_plotting = ["subject_type", "subject_name", "project", "plot_mech_group"]; for (const index in required_params_for_plotting) { const param = required_params_for_plotting[index]; if (this.eval_params[param] === null) { @@ -112,10 +113,10 @@ export default { }, "db_collection_prefix": function () { - if (this.eval_params.project === null || this.eval_params.browser_name === null) { + if (this.eval_params.project === null || this.eval_params.subject_name === null) { return ""; } - return this.eval_params.project.toLowerCase() + "_" + this.eval_params.browser_name.toLowerCase(); + return this.eval_params.project.toLowerCase() + "_" + this.eval_params.subject_name.toLowerCase(); }, "db_collection": function () { if (this.db_collection_suffix === "") { @@ -134,6 +135,13 @@ export default { return `Connecting to database...`; } }, + "subjects": function () { + if (this.eval_params.subject_type !== null) { + return this.subject_availability[this.eval_params.subject_type]; + } else { + return []; + } + }, }, watch: { "selected.experiment": function (val) { @@ -178,7 +186,13 @@ export default { } this.propagate_new_params(); }, - "eval_params.browser_name": function (val) { + "eval_params.subject_type": { + handler(val) { + this.get_projects(); + }, + immediate: true + }, + "eval_params.subject_name": function (val) { // db_collection gets updated too late, so updating manually. this.eval_params.db_collection = this.db_collection; this.propagate_new_params(); @@ -204,8 +218,8 @@ export default { }, created: function () { this.websocket = this.create_socket(); + this.get_subject_support(); this.get_projects(); - this.get_browser_support(); const path = `/api/poc/domain/`; axios.get(path) .then((res) => { @@ -214,12 +228,12 @@ export default { } }) this.timer = setInterval(() => { + if (this.subject_availability.length == 0) { + this.get_subject_support(); + } if (this.projects.length == 0) { this.get_projects(); } - if (this.browsers.length == 0) { - this.get_browser_support(); - } if (this.system == null) { this.get_system_info(); } @@ -267,7 +281,7 @@ export default { if (data.update.hasOwnProperty("plot_data")) { const revision_data = data.update.plot_data.revision_data; const version_data = data.update.plot_data.version_data; - this.$refs.gantt.update_plot(this.eval_params.browser_name, revision_data, version_data); + this.$refs.gantt.update_plot(this.eval_params.subject_name, revision_data, version_data); this.results.nb_of_evaluations = revision_data.outcome.length + version_data.outcome.length; } if (data.update.hasOwnProperty("experiments")) { @@ -316,8 +330,12 @@ export default { localStorage.setItem('theme', 'light'); } }, + update_subject_type(event) { + const selected_type = event.target.value; + this.eval_params.subject_type = selected_type; + }, get_projects(cb) { - const path = `/api/projects/`; + const path = `/api/poc/${this.eval_params.subject_type}/`; axios.get(path) .then((res) => { if (res.data.status == "OK") { @@ -331,12 +349,12 @@ export default { console.error(error); }); }, - get_browser_support() { - const path = `/api/browsers/`; + get_subject_support() { + const path = `/api/subject/`; axios.get(path) .then((res) => { if (res.data.status == "OK") { - this.browsers = res.data.browsers; + this.subject_availability = res.data.subject_availability; } }) .catch((error) => { @@ -367,11 +385,11 @@ export default { "new_params": this.eval_params } ); - } else if (this.eval_params.browser_name !== null) { - console.log('Propagating browser change'); + } else if (this.eval_params.subject_name !== null) { + console.log('Propagating subject change'); this.send_with_socket( { - "new_browser": this.eval_params + "new_subject": this.eval_params } ); } else { @@ -395,13 +413,13 @@ export default { this.eval_params.tests = []; this.select_all_tests = false; }, - set_curr_browser(browser) { - this.eval_params.browser_name = browser["name"]; - this.curr_options.min_browser_version = browser["min_version"]; - this.curr_options.max_browser_version = browser["max_version"]; - this.slider.state = [browser["min_version"], browser["max_version"]]; + set_curr_subject(subject) { + this.eval_params.subject_name = subject["name"]; + this.curr_options.min_subject_version = subject["min_version"]; + this.curr_options.max_subject_version = subject["max_version"]; + this.slider.state = [subject["min_version"], subject["max_version"]]; this.slider.disabled = false; - this.browser_settings = browser['options']; + this.subject_settings = subject['options']; }, submit_form() { const path = `/api/evaluation/start/`; @@ -437,7 +455,7 @@ export default { }) }, create_new_experiment() { - const url = `/api/poc/${this.selected.project}/`; + const url = `/api/poc/${this.eval_params.subject_type}/${this.selected.project}/`; axios.post(url, {'poc_name': this.dialog.new_experiment_name}) .then((res) => { if (res.data.status === "OK") { @@ -451,7 +469,7 @@ export default { }); }, create_new_project() { - const url = `/api/projects/`; + const url = `/api/poc/${this.eval_params.subject_type}/`; const new_project_name = this.dialog.new_project_name; axios.post(url, {'project_name': new_project_name}) .then((res) => { @@ -484,6 +502,12 @@ export default {

{{ banner_message }}

+ +