示例#1
0
    def testStorageAndHydraulicExports(self, project: _Project, request: _pt.FixtureRequest):
        helper = _Helper(project)
        helper.setup()

        # The following line is required otherwise QT will crash
        application = _qtw.QApplication([])

        def quitApplication():
            application.quit()

        request.addfinalizer(quitApplication)

        projectFolderPath = helper.actualProjectFolderPath

        self._exportHydraulic(projectFolderPath, _format="mfs")
        mfsDdckRelativePath = f"{project.projectName}_mfs.dck"
        helper.ensureFilesAreEqual(mfsDdckRelativePath, shallReplaceRandomizedFlowRates=True)

        self._exportHydraulic(projectFolderPath, _format="ddck")
        hydraulicDdckRelativePath = "ddck/hydraulic/hydraulic.ddck"
        helper.ensureFilesAreEqual(hydraulicDdckRelativePath, shallReplaceRandomizedFlowRates=False)

        storageTankNames = self._exportStorageTanksAndGetNames(projectFolderPath)
        for storageTankName in storageTankNames:
            ddckFileRelativePath = f"ddck/{storageTankName}/{storageTankName}.ddck"
            helper.ensureFilesAreEqual(ddckFileRelativePath, shallReplaceRandomizedFlowRates=False)

            ddcxFileRelativePath = f"ddck/{storageTankName}/{storageTankName}.ddcx"
            helper.ensureFilesAreEqual(ddcxFileRelativePath, shallReplaceRandomizedFlowRates=False)
示例#2
0
def bokeh_server(request: pytest.FixtureRequest, log_file: IO[str]) -> str:
    bokeh_port: int = request.config.option.bokeh_port

    cmd = ["python", "-m", "bokeh", "serve"]
    argv = [f"--port={bokeh_port}"]
    bokeh_server_url = f"http://localhost:{bokeh_port}"

    env = os.environ.copy()
    env['BOKEH_MINIFIED'] = 'false'

    try:
        proc = subprocess.Popen(cmd + argv,
                                env=env,
                                stdout=log_file,
                                stderr=log_file)
    except OSError:
        write(f"Failed to run: {' '.join(cmd + argv)}")
        sys.exit(1)
    else:
        # Add in the clean-up code
        def stop_bokeh_server() -> None:
            write("Shutting down bokeh-server ...")
            proc.kill()

        request.addfinalizer(stop_bokeh_server)

        def wait_until(func: Callable[[], Any],
                       timeout: float = 5.0,
                       interval: float = 0.01) -> bool:
            start = time.time()

            while True:
                if func():
                    return True
                if time.time() - start > timeout:
                    return False
                time.sleep(interval)

        def wait_for_bokeh_server() -> bool:
            def helper() -> Any:
                if proc.returncode is not None:
                    return True
                try:  # type: ignore[unreachable] # XXX: typeshed bug, proc.returncode: int
                    return requests.get(bokeh_server_url)
                except ConnectionError:
                    return False

            return wait_until(helper)

        if not wait_for_bokeh_server():
            write(f"Timeout when running: {' '.join(cmd + argv)}")
            sys.exit(1)

        if proc.returncode is not None:
            write(f"bokeh server exited with code {proc.returncode}")
            sys.exit(1)

        return bokeh_server_url  # type: ignore[unreachable] # XXX: typeshed bug, proc.returncode: int
示例#3
0
def remote_repos_path(user_path: pathlib.Path,
                      request: pytest.FixtureRequest) -> pathlib.Path:
    """System's remote (file-based) repos to clone andpush to. Emphemeral directory."""
    dir = user_path / "remote_repos"
    dir.mkdir(exist_ok=True)

    def clean() -> None:
        shutil.rmtree(dir)

    request.addfinalizer(clean)
    return dir
示例#4
0
def projects_path(user_path: pathlib.Path,
                  request: pytest.FixtureRequest) -> pathlib.Path:
    """User's local checkouts and clones. Emphemeral directory."""
    dir = user_path / "projects"
    dir.mkdir(exist_ok=True)

    def clean() -> None:
        shutil.rmtree(dir)

    request.addfinalizer(clean)
    return dir
示例#5
0
def screenshoter(browser: WebDriver, request: pytest.FixtureRequest):
    class Screenshoter:
        def save(self):
            if SCREENSHOTS_FOLDER is None:
                return
            assert browser.save_screenshot(
                f'{SCREENSHOTS_FOLDER}/{time.time_ns()}_{request.node.name}.png'
            )

    instance = Screenshoter()
    request.addfinalizer(lambda: instance.save())
    return instance
示例#6
0
文件: project.py 项目: zebulon2/bokeh
def test_file_path_and_url(request: pytest.FixtureRequest,
                           file_server: SimpleWebServer) -> Tuple[str, str]:
    filename = request.function.__name__ + '.html'
    file_obj = request.fspath.dirpath().join(filename)
    file_path = file_obj.strpath
    url = file_path.replace('\\', '/')  # Windows-proof

    def tear_down() -> None:
        if file_obj.isfile():
            file_obj.remove()

    request.addfinalizer(tear_down)

    return file_path, file_server.where_is(url)
示例#7
0
    def redis_proc_fixture(request: FixtureRequest,
                           tmp_path_factory: TempPathFactory):
        """
        Fixture for pytest-redis.

        #. Get configs.
        #. Run redis process.
        #. Stop redis process after tests.

        :param request: fixture request object
        :param tmpdir_factory:
        :rtype: pytest_redis.executors.TCPExecutor
        :returns: tcp executor
        """
        config = get_config(request)
        redis_exec = executable or config["exec"]
        rdbcompression = config[
            "compression"] if compression is None else compression
        rdbchecksum = config["rdbchecksum"] if checksum is None else checksum

        if datadir:
            redis_datadir = Path(datadir)
        elif config["datadir"]:
            redis_datadir = Path(config["datadir"])
        else:
            redis_datadir = tmp_path_factory.mktemp(
                f"pytest-redis-{request.fixturename}")

        redis_executor = RedisExecutor(
            executable=redis_exec,
            databases=db_count or config["db_count"],
            redis_timeout=timeout or config["timeout"],
            loglevel=loglevel or config["loglevel"],
            rdbcompression=rdbcompression,
            rdbchecksum=rdbchecksum,
            syslog_enabled=syslog or config["syslog"],
            save=save or config["save"],
            host=host or config["host"],
            port=get_port(port) or get_port(config["port"]),
            timeout=60,
            datadir=redis_datadir,
        )
        redis_executor.start()
        request.addfinalizer(redis_executor.stop)

        return redis_executor
示例#8
0
文件: project.py 项目: zebulon2/bokeh
def output_file_url(request: pytest.FixtureRequest,
                    file_server: SimpleWebServer) -> str:
    from bokeh.io import output_file
    filename = request.function.__name__ + '.html'
    file_obj = request.fspath.dirpath().join(filename)
    file_path = file_obj.strpath
    url = file_path.replace('\\', '/')  # Windows-proof

    output_file(file_path, mode='inline')

    def tear_down() -> None:
        if file_obj.isfile():
            file_obj.remove()

    request.addfinalizer(tear_down)

    return file_server.where_is(url)
def _push_request_context(request: pytest.FixtureRequest):
    """During tests execution request context has been pushed, e.g. `url_for`,
    `session`, etc. can be used in tests as is::

        def test_app(app, client):
            assert client.get(url_for('myview')).status_code == 200

    """
    if "app" not in request.fixturenames:
        return
    app = request.getfixturevalue("app")
    ctx = app.test_request_context()
    ctx.push()

    def teardown():
        ctx.pop()

    request.addfinalizer(teardown)
示例#10
0
async def session_context(
    request: pytest.FixtureRequest,
    event_loop: AbstractEventLoop,
) -> contextvars.Context:
    if "nosession" not in request.keywords:
        await initialize_connection("spellbot-test", use_transaction=True)

        test_session = db_session_maker()
        DatabaseSession.set(test_session)  # type: ignore

        BlockFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore
        ChannelFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore
        ConfigFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore
        GameFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore
        GuildAwardFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore
        GuildFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore
        PlayFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore
        UserAwardFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore
        UserFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore
        VerifyFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore
        WatchFactory._meta.sqlalchemy_session = DatabaseSession  # type: ignore

        def cleanup_session():
            async def finalizer() -> None:
                try:
                    await rollback_transaction()
                except Exception:  # pragma: no cover
                    pass

            event_loop.run_until_complete(finalizer())

        request.addfinalizer(cleanup_session)

    context = contextvars.copy_context()

    def cleanup_context():
        nonlocal context
        for c in context:
            c.set(context[c])

    request.addfinalizer(cleanup_context)

    return context
示例#11
0
def reports_test_dir(request: pytest.FixtureRequest) -> str:
    """Returns the relative path to the test reports directory.

    Also, adds a finaliser function to remove the test reports directory and its contents
    once the test has completed.

    Args:
        request (pytest.FixtureRequest): A pytest fixture providing information of the requesting test function.

    Returns:
        str: The relative path to the test reports directory.
    """
    # Declare relative path to the test reports directory
    # to be created in the relevant tests
    reports_test_dir_path = "./src/test/reports"
    # Add finalizer function to remove the test reports directory
    # and its contents once the test has finished
    def remove_reports_dir_contents():
        shutil.rmtree(reports_test_dir_path)

    # Add finalizer function
    request.addfinalizer(remove_reports_dir_contents)
    return reports_test_dir_path
示例#12
0
def file_server(request: pytest.FixtureRequest) -> SimpleWebServer:
    server = SimpleWebServer()
    server.start()
    request.addfinalizer(server.stop)
    return server
示例#13
0
async def dashboard(redis_cache: redis_utils.RedisCache,
                    request: pytest.FixtureRequest) -> DashboardFixture:
    is_unittest_class = request.cls is not None
    subscription_active = False
    marker = request.node.get_closest_marker("subscription")
    if marker:
        subscription_active = marker.args[0]
    elif is_unittest_class:
        subscription_active = request.cls.SUBSCRIPTION_ACTIVE

    api_key_admin = "a" * 64

    sub = subscription.Subscription(
        redis_cache,
        config.TESTING_ORGANIZATION_ID,
        "You're not nice",
        frozenset(
            getattr(subscription.Features, f)
            for f in subscription.Features.__members__) if subscription_active
        else frozenset([subscription.Features.PUBLIC_REPOSITORY]),
    )
    await sub._save_subscription_to_cache()
    user_tokens = user_tokens_mod.UserTokens(
        redis_cache,
        config.TESTING_ORGANIZATION_ID,
        [
            {
                "id": github_types.GitHubAccountIdType(config.ORG_ADMIN_ID),
                "login": github_types.GitHubLogin("mergify-test1"),
                "oauth_access_token": config.ORG_ADMIN_GITHUB_APP_OAUTH_TOKEN,
                "name": None,
                "email": None,
            },
            {
                "id": github_types.GitHubAccountIdType(config.ORG_USER_ID),
                "login": github_types.GitHubLogin("mergify-test4"),
                "oauth_access_token": config.ORG_USER_PERSONAL_TOKEN,
                "name": None,
                "email": None,
            },
        ],
    )
    await typing.cast(user_tokens_mod.UserTokensSaas,
                      user_tokens).save_to_cache()

    real_get_subscription = subscription.Subscription.get_subscription

    async def fake_retrieve_subscription_from_db(redis_cache, owner_id):
        if owner_id == config.TESTING_ORGANIZATION_ID:
            return sub
        return subscription.Subscription(
            redis_cache,
            owner_id,
            "We're just testing",
            set(subscription.Features.PUBLIC_REPOSITORY),
        )

    async def fake_subscription(redis_cache, owner_id):
        if owner_id == config.TESTING_ORGANIZATION_ID:
            return await real_get_subscription(redis_cache, owner_id)
        return subscription.Subscription(
            redis_cache,
            owner_id,
            "We're just testing",
            set(subscription.Features.PUBLIC_REPOSITORY),
        )

    patcher = mock.patch(
        "mergify_engine.dashboard.subscription.Subscription._retrieve_subscription_from_db",
        side_effect=fake_retrieve_subscription_from_db,
    )
    patcher.start()
    request.addfinalizer(patcher.stop)

    patcher = mock.patch(
        "mergify_engine.dashboard.subscription.Subscription.get_subscription",
        side_effect=fake_subscription,
    )
    patcher.start()
    request.addfinalizer(patcher.stop)

    async def fake_retrieve_user_tokens_from_db(redis_cache, owner_id):
        if owner_id == config.TESTING_ORGANIZATION_ID:
            return user_tokens
        return user_tokens_mod.UserTokens(redis_cache, owner_id, {})

    real_get_user_tokens = user_tokens_mod.UserTokens.get

    async def fake_user_tokens(redis_cache, owner_id):
        if owner_id == config.TESTING_ORGANIZATION_ID:
            return await real_get_user_tokens(redis_cache, owner_id)
        return user_tokens_mod.UserTokens(redis_cache, owner_id, {})

    patcher = mock.patch(
        "mergify_engine.dashboard.user_tokens.UserTokensSaas._retrieve_from_db",
        side_effect=fake_retrieve_user_tokens_from_db,
    )
    patcher.start()
    request.addfinalizer(patcher.stop)

    patcher = mock.patch(
        "mergify_engine.dashboard.user_tokens.UserTokensSaas.get",
        side_effect=fake_user_tokens,
    )
    patcher.start()
    request.addfinalizer(patcher.stop)

    async def fake_application_get(redis_cache, api_access_key, api_secret_key,
                                   account_scope):
        if (api_access_key == api_key_admin[:32]
                and api_secret_key == api_key_admin[32:]):
            return application_mod.Application(
                redis_cache,
                123,
                "testing application",
                api_access_key,
                api_secret_key,
                account_scope={
                    "id": config.TESTING_ORGANIZATION_ID,
                    "login": config.TESTING_ORGANIZATION_NAME,
                },
            )
        raise application_mod.ApplicationUserNotFound()

    patcher = mock.patch(
        "mergify_engine.dashboard.application.ApplicationSaas.get",
        side_effect=fake_application_get,
    )
    patcher.start()
    request.addfinalizer(patcher.stop)

    return DashboardFixture(
        api_key_admin,
        sub,
        user_tokens,
    )
示例#14
0
async def recorder(
    request: pytest.FixtureRequest, ) -> typing.Optional[RecorderFixture]:
    is_unittest_class = request.cls is not None

    marker = request.node.get_closest_marker("recorder")
    if not is_unittest_class and marker is None:
        return None

    if is_unittest_class:
        cassette_library_dir = os.path.join(
            CASSETTE_LIBRARY_DIR_BASE,
            request.cls.__name__,
            request.node.name,
        )
    else:
        cassette_library_dir = os.path.join(
            CASSETTE_LIBRARY_DIR_BASE,
            request.node.module.__name__.replace(
                "mergify_engine.tests.functional.", "").replace(".", "/"),
            request.node.name,
        )

    # Recording stuffs
    if RECORD:
        if os.path.exists(cassette_library_dir):
            shutil.rmtree(cassette_library_dir)
        os.makedirs(cassette_library_dir)

    recorder = vcr.VCR(
        cassette_library_dir=cassette_library_dir,
        record_mode="all" if RECORD else "none",
        match_on=["method", "uri"],
        ignore_localhost=True,
        filter_headers=[
            ("Authorization", "<TOKEN>"),
            ("X-Hub-Signature", "<SIGNATURE>"),
            ("User-Agent", None),
            ("Accept-Encoding", None),
            ("Connection", None),
        ],
        before_record_response=pyvcr_response_filter,
        before_record_request=pyvcr_request_filter,
    )

    if RECORD:
        github.CachedToken.STORAGE = {}
    else:
        # Never expire token during replay
        patcher = mock.patch.object(github_app,
                                    "get_or_create_jwt",
                                    return_value="<TOKEN>")
        patcher.start()
        request.addfinalizer(patcher.stop)
        patcher = mock.patch.object(
            github.GithubAppInstallationAuth,
            "get_access_token",
            return_value="<TOKEN>",
        )
        patcher.start()
        request.addfinalizer(patcher.stop)

    # Let's start recording
    cassette = recorder.use_cassette("http.json")
    cassette.__enter__()
    request.addfinalizer(cassette.__exit__)
    record_config_file = os.path.join(cassette_library_dir, "config.json")

    if RECORD:
        with open(record_config_file, "w") as f:
            f.write(
                json.dumps(
                    RecordConfigType({
                        "organization_id":
                        config.TESTING_ORGANIZATION_ID,
                        "organization_name":
                        config.TESTING_ORGANIZATION_NAME,
                        "repository_id":
                        config.TESTING_REPOSITORY_ID,
                        "repository_name":
                        github_types.GitHubRepositoryName(
                            config.TESTING_REPOSITORY_NAME),
                        "branch_prefix":
                        datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S"),
                    })))

    with open(record_config_file, "r") as f:
        return RecorderFixture(
            typing.cast(RecordConfigType, json.loads(f.read())), recorder)
示例#15
0
def app(request: pytest.FixtureRequest) -> "SecurityFixture":
    app = SecurityFixture(__name__)
    app.response_class = Response
    app.debug = True
    app.config["SECRET_KEY"] = "secret"
    app.config["TESTING"] = True
    app.config["LOGIN_DISABLED"] = False
    app.config["WTF_CSRF_ENABLED"] = False
    # Our test emails/domain isn't necessarily valid
    app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": False}
    app.config["SECURITY_TWO_FACTOR_SECRET"] = {
        "1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B"
    }
    app.config["SECURITY_SMS_SERVICE"] = "test"
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

    app.config["SECURITY_PASSWORD_SALT"] = "salty"
    # Make this plaintext for most tests - reduces unit test time by 50%
    app.config["SECURITY_PASSWORD_HASH"] = "plaintext"
    # Make this hex_md5 for token tests
    app.config["SECURITY_HASHING_SCHEMES"] = ["hex_md5"]
    app.config["SECURITY_DEPRECATED_HASHING_SCHEMES"] = []

    for opt in [
        "changeable",
        "recoverable",
        "registerable",
        "trackable",
        "passwordless",
        "confirmable",
        "two_factor",
        "unified_signin",
        "webauthn",
    ]:
        app.config["SECURITY_" + opt.upper()] = opt in request.keywords

    pytest_major = int(pytest.__version__.split(".")[0])
    if pytest_major >= 4:
        marker_getter = request.node.get_closest_marker
    else:
        marker_getter = request.keywords.get
    settings = marker_getter("settings")
    if settings is not None:
        for key, value in settings.kwargs.items():
            app.config["SECURITY_" + key.upper()] = value

    app.mail = Mail(app)  # type: ignore
    app.json_encoder = JSONEncoder

    # use babel marker to signify tests that need babel extension.
    babel = marker_getter("babel")
    if babel:
        if NO_BABEL:
            raise pytest.skip("Requires Babel")
        Babel(app)

    @app.route("/")
    def index():
        return render_template("index.html", content="Home Page")

    @app.route("/profile")
    @auth_required()
    def profile():
        if hasattr(app, "security"):
            if app.security._want_json(flask_request):
                return jsonify(message="profile")

        return render_template("index.html", content="Profile Page")

    @app.route("/post_login")
    @login_required
    def post_login():
        return render_template("index.html", content="Post Login")

    @app.route("/http")
    @http_auth_required
    def http():
        return "HTTP Authentication"

    @app.route("/http_admin_required")
    @http_auth_required
    @permissions_required("admin")
    def http_admin_required():
        assert get_request_attr("fs_authn_via") == "basic"
        return "HTTP Authentication"

    @app.route("/http_custom_realm")
    @http_auth_required("My Realm")
    def http_custom_realm():
        assert get_request_attr("fs_authn_via") == "basic"
        return render_template("index.html", content="HTTP Authentication")

    @app.route("/token", methods=["GET", "POST"])
    @auth_token_required
    def token():
        assert get_request_attr("fs_authn_via") == "token"
        return render_template("index.html", content="Token Authentication")

    @app.route("/multi_auth")
    @auth_required("session", "token", "basic")
    def multi_auth():
        return render_template("index.html", content="Session, Token, Basic auth")

    @app.route("/post_logout")
    def post_logout():
        return render_template("index.html", content="Post Logout")

    @app.route("/post_register")
    def post_register():
        return render_template("index.html", content="Post Register")

    @app.route("/post_confirm")
    def post_confirm():
        return render_template("index.html", content="Post Confirm")

    @app.route("/admin")
    @roles_required("admin")
    def admin():
        assert get_request_attr("fs_authn_via") == "session"
        return render_template("index.html", content="Admin Page")

    @app.route("/admin_and_editor")
    @roles_required("admin", "editor")
    def admin_and_editor():
        return render_template("index.html", content="Admin and Editor Page")

    @app.route("/admin_or_editor")
    @roles_accepted("admin", "editor")
    def admin_or_editor():
        return render_template("index.html", content="Admin or Editor Page")

    @app.route("/simple")
    @roles_accepted("simple")
    def simple():
        return render_template("index.html", content="SimplePage")

    @app.route("/admin_perm")
    @permissions_accepted("full-write", "super")
    def admin_perm():
        return render_template(
            "index.html", content="Admin Page with full-write or super"
        )

    @app.route("/admin_perm_required")
    @permissions_required("full-write", "super")
    def admin_perm_required():
        return render_template("index.html", content="Admin Page required")

    @app.route("/page1")
    def page_1():
        return "Page 1"

    @app.route("/json", methods=["GET", "POST"])
    def echo_json():
        return jsonify(flask_request.get_json())

    @app.route("/unauthz", methods=["GET", "POST"])
    def unauthz():
        return render_template("index.html", content="Unauthorized")

    @app.route("/fresh", methods=["GET", "POST"])
    @auth_required(within=60)
    def fresh():
        if app.security._want_json(flask_request):
            return jsonify(title="Fresh Only")
        else:
            return render_template("index.html", content="Fresh Only")

    def revert_forms():
        # Some forms/tests have dynamic fields - be sure to revert them.
        if hasattr(app, "security"):
            if hasattr(app.security.login_form, "email"):
                del app.security.login_form.email
            if hasattr(app.security.register_form, "username"):
                del app.security.register_form.username
            if hasattr(app.security.confirm_register_form, "username"):
                del app.security.confirm_register_form.username

    request.addfinalizer(revert_forms)
    return app
示例#16
0
def jupyter_notebook(request: pytest.FixtureRequest, log_file: IO[str]) -> str:
    """
    Starts a jupyter notebook server at the beginning of a session, and
    closes at the end of a session.

    Adds custom.js that runs all the cells on notebook opening. Cleans out
    this custom.js at the end of the test run.

    Returns the url that the jupyter notebook is running at.

    """

    # First - set-up the notebooks to run all cells when they're opened
    #
    # Can be cleaned up further to remember the user's existing customJS
    # and then restore it after the test run.
    from jupyter_core import paths
    config_dir = paths.jupyter_config_dir()

    body = """
require(["base/js/namespace", "base/js/events"], function (IPython, events) {
    events.on("kernel_ready.Kernel", function () {
        IPython.notebook.execute_all_cells();
    });
});
"""
    custom = join(config_dir, "custom")
    if not exists(custom):
        os.makedirs(custom)

    customjs = join(custom, "custom.js")

    old_customjs = None
    if exists(customjs):
        with open(customjs) as f:
            old_customjs = f.read()

    with open(customjs, "w") as f:
        f.write(body)

    # Add in the clean-up code
    def clean_up_customjs() -> None:
        text = old_customjs if old_customjs is not None else ""
        with open(customjs, "w") as f:
            f.write(text)

    request.addfinalizer(clean_up_customjs)

    # Second - Run a notebook server at the examples directory
    #

    notebook_port = request.config.option.notebook_port

    env = os.environ.copy()
    env['BOKEH_RESOURCES'] = 'server'

    # Launch from the base directory of bokeh repo
    notebook_dir = join(dirname(__file__), pardir, pardir)

    cmd = ["jupyter", "notebook"]
    argv = [
        "--no-browser", f"--port={notebook_port}",
        f"--notebook-dir={notebook_dir}"
    ]
    jupter_notebook_url = f"http://localhost:{notebook_port}"

    try:
        proc = subprocess.Popen(cmd + argv,
                                env=env,
                                stdout=log_file,
                                stderr=log_file)
    except OSError:
        write(f"Failed to run: {' '.join(cmd + argv)}")
        sys.exit(1)
    else:
        # Add in the clean-up code
        def stop_jupyter_notebook() -> None:
            write("Shutting down jupyter-notebook ...")
            proc.kill()

        request.addfinalizer(stop_jupyter_notebook)

        def wait_until(func: Callable[[], Any],
                       timeout: float = 5.0,
                       interval: float = 0.01) -> bool:
            start = time.time()

            while True:
                if func():
                    return True
                if time.time() - start > timeout:
                    return False
                time.sleep(interval)

        def wait_for_jupyter_notebook() -> bool:
            def helper() -> Any:
                if proc.returncode is not None:
                    return True
                try:  # type: ignore[unreachable] # XXX: typeshed bug, proc.returncode: int
                    return requests.get(jupter_notebook_url)
                except ConnectionError:
                    return False

            return wait_until(helper)

        if not wait_for_jupyter_notebook():
            write(f"Timeout when running: {' '.join(cmd + argv)}")
            sys.exit(1)

        if proc.returncode is not None:
            write(f"Jupyter notebook exited with code {proc.returncode}")
            sys.exit(1)

        return jupter_notebook_url  # type: ignore[unreachable] # XXX: typeshed bug, proc.returncode: int