chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import random
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
BASE_DIR = "/tmp/test-git"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_sandbox(sandbox_factory):
|
||||
return sandbox_factory(timeout=10)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_author():
|
||||
return "Sandbox Bot", "sandbox@example.com"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_credentials():
|
||||
return "git", "token", "example.com", "https"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_base_dir(git_sandbox):
|
||||
base_dir = f"{BASE_DIR}/{uuid4().hex}"
|
||||
git_sandbox.commands.run(f'rm -rf "{base_dir}" && mkdir -p "{base_dir}"')
|
||||
yield base_dir
|
||||
git_sandbox.commands.run(f'rm -rf "{base_dir}"')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_repo(git_sandbox, git_base_dir, git_author):
|
||||
repo_path = f"{git_base_dir}/repo"
|
||||
git_sandbox.git.init(repo_path, initial_branch="main")
|
||||
author_name, author_email = git_author
|
||||
git_sandbox.git.configure_user(author_name, author_email)
|
||||
return repo_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_repo_with_commit(git_sandbox, git_repo, git_author):
|
||||
author_name, author_email = git_author
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
git_sandbox.git.add(git_repo, all=True)
|
||||
git_sandbox.git.commit(
|
||||
git_repo,
|
||||
message="Initial commit",
|
||||
author_name=author_name,
|
||||
author_email=author_email,
|
||||
)
|
||||
return git_repo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_daemon(git_sandbox, git_base_dir):
|
||||
remote_path = f"{git_base_dir}/remote.git"
|
||||
git_sandbox.commands.run(f'git init --bare --initial-branch=main "{remote_path}"')
|
||||
port = 9418 + random.randint(0, 1000)
|
||||
cmd = (
|
||||
f'git daemon --reuseaddr --base-path="{git_base_dir}" --export-all '
|
||||
f"--enable=receive-pack --informative-errors --listen=127.0.0.1 --port={port}"
|
||||
)
|
||||
handle = git_sandbox.commands.run(cmd, background=True)
|
||||
git_sandbox.commands.run("sleep 1")
|
||||
try:
|
||||
yield {
|
||||
"remote_path": remote_path,
|
||||
"remote_url": f"git://127.0.0.1:{port}/remote.git",
|
||||
"port": port,
|
||||
"base_dir": git_base_dir,
|
||||
}
|
||||
finally:
|
||||
handle.kill()
|
||||
@@ -0,0 +1,16 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_add_stages_files(git_sandbox, git_repo):
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
|
||||
git_sandbox.git.add(git_repo, all=True)
|
||||
|
||||
status = git_sandbox.git.status(git_repo)
|
||||
entry = next(
|
||||
(item for item in status.file_status if item.name == "README.md"), None
|
||||
)
|
||||
assert entry is not None
|
||||
assert entry.status == "added"
|
||||
assert entry.staged is True
|
||||
@@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
|
||||
from e2b.exceptions import InvalidArgumentException
|
||||
from e2b.sandbox._git import (
|
||||
build_remote_add_args,
|
||||
build_remote_get_command,
|
||||
build_reset_args,
|
||||
)
|
||||
|
||||
|
||||
def test_build_reset_args_rejects_invalid_mode():
|
||||
with pytest.raises(InvalidArgumentException) as exc:
|
||||
build_reset_args("bogus", None, None)
|
||||
# Order must match the JS SDK exactly.
|
||||
assert "Reset mode must be one of soft, mixed, hard, merge, keep." in str(exc.value)
|
||||
|
||||
|
||||
def test_build_reset_args_accepts_valid_mode():
|
||||
assert build_reset_args("hard", "HEAD", None) == ["reset", "--hard", "HEAD"]
|
||||
|
||||
|
||||
def test_build_remote_add_args_requires_name_and_url():
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
build_remote_add_args("", "https://example.com", False)
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
build_remote_add_args("origin", "", False)
|
||||
|
||||
|
||||
def test_build_remote_get_command_requires_name():
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
build_remote_get_command("/repo", "")
|
||||
@@ -0,0 +1,52 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_branches_lists_current_and_feature(git_sandbox, git_repo_with_commit):
|
||||
repo_path = git_repo_with_commit
|
||||
|
||||
git_sandbox.commands.run(f'git -C "{repo_path}" branch feature')
|
||||
|
||||
branches = git_sandbox.git.branches(repo_path)
|
||||
assert branches.current_branch == "main"
|
||||
assert "main" in branches.branches
|
||||
assert "feature" in branches.branches
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_checkout_branch_switches_branch(git_sandbox, git_repo_with_commit):
|
||||
repo_path = git_repo_with_commit
|
||||
|
||||
git_sandbox.commands.run(f'git -C "{repo_path}" branch feature')
|
||||
git_sandbox.git.checkout_branch(repo_path, "feature")
|
||||
|
||||
head = git_sandbox.commands.run(
|
||||
f'git -C "{repo_path}" rev-parse --abbrev-ref HEAD'
|
||||
).stdout.strip()
|
||||
assert head == "feature"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_create_branch_creates_branch(git_sandbox, git_repo_with_commit):
|
||||
repo_path = git_repo_with_commit
|
||||
|
||||
git_sandbox.git.create_branch(repo_path, "feature")
|
||||
|
||||
branches = git_sandbox.git.branches(repo_path)
|
||||
assert "feature" in branches.branches
|
||||
assert branches.current_branch == "feature"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_delete_branch_removes_branch(git_sandbox, git_repo_with_commit):
|
||||
repo_path = git_repo_with_commit
|
||||
|
||||
git_sandbox.commands.run(f'git -C "{repo_path}" branch feature')
|
||||
git_sandbox.git.delete_branch(repo_path, "feature")
|
||||
|
||||
branch = git_sandbox.commands.run(
|
||||
f'git -C "{repo_path}" branch --list feature'
|
||||
).stdout.strip()
|
||||
branches = git_sandbox.git.branches(repo_path)
|
||||
assert branch == ""
|
||||
assert "feature" not in branches.branches
|
||||
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_clone_fetches_repo(
|
||||
git_sandbox, git_repo_with_commit, git_daemon, git_base_dir
|
||||
):
|
||||
repo_path = git_repo_with_commit
|
||||
remote_url = git_daemon["remote_url"]
|
||||
clone_path = f"{git_base_dir}/clone"
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
git_sandbox.git.push(
|
||||
repo_path,
|
||||
remote="origin",
|
||||
branch="main",
|
||||
set_upstream=True,
|
||||
)
|
||||
|
||||
git_sandbox.git.clone(remote_url, clone_path)
|
||||
contents = git_sandbox.files.read(f"{clone_path}/README.md")
|
||||
assert "hello" in contents
|
||||
@@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_commit_creates_commit(git_sandbox, git_repo, git_author):
|
||||
git_sandbox.set_timeout(20)
|
||||
|
||||
author_name, author_email = git_author
|
||||
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
git_sandbox.git.add(git_repo, all=True)
|
||||
git_sandbox.git.commit(
|
||||
git_repo,
|
||||
message="Initial commit",
|
||||
author_name=author_name,
|
||||
author_email=author_email,
|
||||
)
|
||||
|
||||
message = git_sandbox.commands.run(
|
||||
f'git -C "{git_repo}" log -1 --pretty=%B'
|
||||
).stdout.strip()
|
||||
assert message == "Initial commit"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_commit_uses_config_for_missing_author(git_sandbox, git_repo, git_author):
|
||||
_, expected_email = git_author
|
||||
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
git_sandbox.git.add(git_repo, all=True)
|
||||
override_name = "Override Bot"
|
||||
git_sandbox.git.commit(
|
||||
git_repo,
|
||||
message="Partial author commit",
|
||||
author_name=override_name,
|
||||
)
|
||||
|
||||
author_name = git_sandbox.commands.run(
|
||||
f'git -C "{git_repo}" log -1 --pretty=%an'
|
||||
).stdout.strip()
|
||||
logged_email = git_sandbox.commands.run(
|
||||
f'git -C "{git_repo}" log -1 --pretty=%ae'
|
||||
).stdout.strip()
|
||||
|
||||
assert author_name == override_name
|
||||
assert logged_email == expected_email
|
||||
@@ -0,0 +1,52 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_get_config_reads_local_config(git_sandbox, git_repo):
|
||||
git_sandbox.commands.run(f'git -C "{git_repo}" config --local pull.rebase true')
|
||||
|
||||
value = git_sandbox.git.get_config("pull.rebase", scope="local", path=git_repo)
|
||||
command_value = git_sandbox.commands.run(
|
||||
f'git -C "{git_repo}" config --local --get pull.rebase'
|
||||
).stdout.strip()
|
||||
assert value == "true"
|
||||
assert command_value == "true"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_set_config_updates_local_config(git_sandbox, git_repo):
|
||||
git_sandbox.git.set_config(
|
||||
"pull.rebase",
|
||||
"true",
|
||||
scope="local",
|
||||
path=git_repo,
|
||||
)
|
||||
|
||||
value = git_sandbox.commands.run(
|
||||
f'git -C "{git_repo}" config --local --get pull.rebase'
|
||||
).stdout.strip()
|
||||
configured_value = git_sandbox.git.get_config(
|
||||
"pull.rebase", scope="local", path=git_repo
|
||||
)
|
||||
assert value == "true"
|
||||
assert configured_value == "true"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_configure_user_sets_global_config(git_sandbox, git_author):
|
||||
author_name, author_email = git_author
|
||||
|
||||
git_sandbox.git.configure_user(author_name, author_email)
|
||||
|
||||
name = git_sandbox.commands.run(
|
||||
"git config --global --get user.name"
|
||||
).stdout.strip()
|
||||
email = git_sandbox.commands.run(
|
||||
"git config --global --get user.email"
|
||||
).stdout.strip()
|
||||
configured_name = git_sandbox.git.get_config("user.name", scope="global")
|
||||
configured_email = git_sandbox.git.get_config("user.email", scope="global")
|
||||
assert name == author_name
|
||||
assert email == author_email
|
||||
assert configured_name == author_name
|
||||
assert configured_email == author_email
|
||||
@@ -0,0 +1,20 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_dangerously_authenticate_sets_helper(git_sandbox, git_credentials):
|
||||
username, password, host, protocol = git_credentials
|
||||
|
||||
git_sandbox.git.dangerously_authenticate(
|
||||
username,
|
||||
password,
|
||||
host=host,
|
||||
protocol=protocol,
|
||||
)
|
||||
|
||||
helper = git_sandbox.commands.run(
|
||||
"git config --global --get credential.helper"
|
||||
).stdout.strip()
|
||||
configured_helper = git_sandbox.git.get_config("credential.helper", scope="global")
|
||||
assert helper == "store"
|
||||
assert configured_helper == "store"
|
||||
@@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_init_creates_repo(git_sandbox, git_base_dir):
|
||||
repo_path = f"{git_base_dir}/repo"
|
||||
|
||||
git_sandbox.git.init(repo_path, initial_branch="main")
|
||||
|
||||
assert git_sandbox.files.exists(f"{repo_path}/.git")
|
||||
head = git_sandbox.commands.run(
|
||||
f'git -C "{repo_path}" symbolic-ref --short HEAD'
|
||||
).stdout.strip()
|
||||
assert head == "main"
|
||||
@@ -0,0 +1,33 @@
|
||||
import inspect
|
||||
|
||||
from e2b.sandbox_async.git import Git as AsyncGit
|
||||
from e2b.sandbox_sync.git import Git as SyncGit
|
||||
|
||||
|
||||
def _public_methods(cls):
|
||||
return {
|
||||
name: getattr(cls, name)
|
||||
for name in sorted(dir(cls))
|
||||
if not name.startswith("_") and callable(getattr(cls, name))
|
||||
}
|
||||
|
||||
|
||||
def test_identical_method_signatures():
|
||||
sync = _public_methods(SyncGit)
|
||||
async_ = _public_methods(AsyncGit)
|
||||
|
||||
assert set(sync) == set(async_), (
|
||||
f"missing from async: {set(sync) - set(async_)}, "
|
||||
f"missing from sync: {set(async_) - set(sync)}"
|
||||
)
|
||||
|
||||
for name in sync:
|
||||
assert inspect.signature(sync[name]) == inspect.signature(async_[name]), (
|
||||
f"{name}: sync{inspect.signature(sync[name])} "
|
||||
f"!= async{inspect.signature(async_[name])}"
|
||||
)
|
||||
|
||||
|
||||
def test_async_methods_are_coroutines():
|
||||
for name, method in _public_methods(AsyncGit).items():
|
||||
assert inspect.iscoroutinefunction(method), f"AsyncGit.{name} is not async"
|
||||
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_remote_get_returns_none_for_missing_remote(git_sandbox, git_repo):
|
||||
repo_path = git_repo
|
||||
missing_url = git_sandbox.git.remote_get(repo_path, "origin")
|
||||
assert missing_url is None
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_remote_add_adds_remote(git_sandbox, git_repo, git_daemon):
|
||||
repo_path = git_repo
|
||||
remote_url = git_daemon["remote_url"]
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
current_url = git_sandbox.git.remote_get(repo_path, "origin")
|
||||
assert current_url == remote_url
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_remote_add_overwrites_existing_remote(git_sandbox, git_repo, git_daemon):
|
||||
repo_path = git_repo
|
||||
remote_url = git_daemon["remote_url"]
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
current_url = git_sandbox.commands.run(
|
||||
f'git -C "{repo_path}" remote get-url origin'
|
||||
).stdout.strip()
|
||||
current_remote = git_sandbox.git.remote_get(repo_path, "origin")
|
||||
assert current_url == remote_url
|
||||
assert current_remote == remote_url
|
||||
|
||||
second_path = f"{git_daemon['base_dir']}/remote-2.git"
|
||||
git_sandbox.commands.run(f'git init --bare --initial-branch=main "{second_path}"')
|
||||
second_url = f"git://127.0.0.1:{git_daemon['port']}/remote-2.git"
|
||||
git_sandbox.git.remote_add(repo_path, "origin", second_url, overwrite=True)
|
||||
|
||||
updated_url = git_sandbox.commands.run(
|
||||
f'git -C "{repo_path}" remote get-url origin'
|
||||
).stdout.strip()
|
||||
updated_remote = git_sandbox.git.remote_get(repo_path, "origin")
|
||||
assert updated_url == second_url
|
||||
assert updated_remote == second_url
|
||||
@@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_reset_hard_discards_changes(git_sandbox, git_repo_with_commit):
|
||||
git_sandbox.files.write(f"{git_repo_with_commit}/README.md", "changed\n")
|
||||
|
||||
status = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status.is_clean is False
|
||||
|
||||
git_sandbox.git.reset(git_repo_with_commit, mode="hard", target="HEAD")
|
||||
|
||||
status_after = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status_after.is_clean is True
|
||||
|
||||
contents = git_sandbox.files.read(f"{git_repo_with_commit}/README.md")
|
||||
assert contents == "hello\n"
|
||||
@@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_restore_unstages_changes(git_sandbox, git_repo_with_commit):
|
||||
git_sandbox.files.write(f"{git_repo_with_commit}/README.md", "changed\n")
|
||||
git_sandbox.git.add(git_repo_with_commit, files=["README.md"])
|
||||
|
||||
status = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status.has_staged is True
|
||||
|
||||
git_sandbox.git.restore(
|
||||
git_repo_with_commit,
|
||||
paths=["README.md"],
|
||||
staged=True,
|
||||
worktree=False,
|
||||
)
|
||||
|
||||
status_after = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status_after.has_staged is False
|
||||
assert status_after.has_changes is True
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_restore_worktree_discards_changes(git_sandbox, git_repo_with_commit):
|
||||
git_sandbox.files.write(f"{git_repo_with_commit}/README.md", "changed\n")
|
||||
|
||||
status = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status.is_clean is False
|
||||
|
||||
git_sandbox.git.restore(git_repo_with_commit, paths=["README.md"])
|
||||
|
||||
status_after = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status_after.is_clean is True
|
||||
|
||||
contents = git_sandbox.files.read(f"{git_repo_with_commit}/README.md")
|
||||
assert contents == "hello\n"
|
||||
@@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_status_reports_untracked_file(git_sandbox, git_repo):
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
|
||||
status = git_sandbox.git.status(git_repo)
|
||||
entry = next(
|
||||
(item for item in status.file_status if item.name == "README.md"), None
|
||||
)
|
||||
|
||||
assert entry is not None
|
||||
assert entry.status == "untracked"
|
||||
assert status.is_clean is False
|
||||
assert status.has_changes is True
|
||||
assert status.has_untracked is True
|
||||
assert status.has_staged is False
|
||||
assert status.has_conflicts is False
|
||||
assert status.total_count == 1
|
||||
assert status.staged_count == 0
|
||||
assert status.unstaged_count == 1
|
||||
assert status.untracked_count == 1
|
||||
assert status.conflict_count == 0
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_status_reports_added_modified_deleted_renamed(
|
||||
git_sandbox, git_repo, git_author
|
||||
):
|
||||
author_name, author_email = git_author
|
||||
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
git_sandbox.files.write(f"{git_repo}/DELETE.md", "delete me\n")
|
||||
git_sandbox.files.write(f"{git_repo}/RENAME.md", "rename me\n")
|
||||
git_sandbox.git.add(git_repo, all=True)
|
||||
git_sandbox.git.commit(
|
||||
git_repo,
|
||||
message="Initial commit",
|
||||
author_name=author_name,
|
||||
author_email=author_email,
|
||||
)
|
||||
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello again\n")
|
||||
git_sandbox.files.write(f"{git_repo}/NEW.md", "new file\n")
|
||||
git_sandbox.git.add(git_repo, files=["NEW.md"])
|
||||
git_sandbox.commands.run(f'git -C "{git_repo}" rm DELETE.md')
|
||||
git_sandbox.commands.run(f'git -C "{git_repo}" mv RENAME.md RENAMED.md')
|
||||
|
||||
status = git_sandbox.git.status(git_repo)
|
||||
modified = next(
|
||||
(item for item in status.file_status if item.name == "README.md"), None
|
||||
)
|
||||
added = next((item for item in status.file_status if item.name == "NEW.md"), None)
|
||||
deleted = next(
|
||||
(item for item in status.file_status if item.name == "DELETE.md"), None
|
||||
)
|
||||
renamed = next(
|
||||
(item for item in status.file_status if item.name == "RENAMED.md"),
|
||||
None,
|
||||
)
|
||||
|
||||
assert modified is not None
|
||||
assert modified.status == "modified"
|
||||
assert modified.staged is False
|
||||
|
||||
assert added is not None
|
||||
assert added.status == "added"
|
||||
assert added.staged is True
|
||||
|
||||
assert deleted is not None
|
||||
assert deleted.status == "deleted"
|
||||
assert deleted.staged is True
|
||||
|
||||
assert renamed is not None
|
||||
assert renamed.status == "renamed"
|
||||
assert renamed.staged is True
|
||||
assert renamed.renamed_from == "RENAME.md"
|
||||
|
||||
assert status.has_changes is True
|
||||
assert status.has_staged is True
|
||||
assert status.has_untracked is False
|
||||
assert status.has_conflicts is False
|
||||
assert status.total_count == 4
|
||||
assert status.staged_count == 3
|
||||
assert status.unstaged_count == 1
|
||||
assert status.untracked_count == 0
|
||||
assert status.conflict_count == 0
|
||||
@@ -0,0 +1,81 @@
|
||||
import pytest
|
||||
|
||||
from e2b.exceptions import GitUpstreamException
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_push_updates_remote(git_sandbox, git_repo_with_commit, git_daemon):
|
||||
repo_path = git_repo_with_commit
|
||||
remote_url = git_daemon["remote_url"]
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
git_sandbox.git.push(
|
||||
repo_path,
|
||||
remote="origin",
|
||||
branch="main",
|
||||
set_upstream=True,
|
||||
)
|
||||
|
||||
message = git_sandbox.commands.run(
|
||||
f'git --git-dir="{git_daemon["remote_path"]}" log -1 --pretty=%B'
|
||||
).stdout.strip()
|
||||
assert message == "Initial commit"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_push_without_upstream_warns(git_sandbox, git_repo_with_commit, git_daemon):
|
||||
repo_path = git_repo_with_commit
|
||||
remote_url = git_daemon["remote_url"]
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
|
||||
with pytest.raises(GitUpstreamException) as exc:
|
||||
git_sandbox.git.push(repo_path, set_upstream=False)
|
||||
|
||||
assert "no upstream branch is configured" in str(exc.value).lower()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_pull_updates_clone(
|
||||
git_sandbox, git_repo_with_commit, git_daemon, git_base_dir, git_author
|
||||
):
|
||||
repo_path = git_repo_with_commit
|
||||
remote_url = git_daemon["remote_url"]
|
||||
clone_path = f"{git_base_dir}/clone"
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
git_sandbox.git.push(
|
||||
repo_path,
|
||||
remote="origin",
|
||||
branch="main",
|
||||
set_upstream=True,
|
||||
)
|
||||
git_sandbox.git.clone(remote_url, clone_path)
|
||||
|
||||
git_sandbox.files.write(f"{repo_path}/README.md", "hello\nmore\n")
|
||||
author_name, author_email = git_author
|
||||
git_sandbox.git.add(repo_path, all=True)
|
||||
git_sandbox.git.commit(
|
||||
repo_path,
|
||||
message="Update README",
|
||||
author_name=author_name,
|
||||
author_email=author_email,
|
||||
)
|
||||
git_sandbox.git.push(repo_path)
|
||||
|
||||
git_sandbox.git.pull(clone_path)
|
||||
contents = git_sandbox.files.read(f"{clone_path}/README.md")
|
||||
assert "more" in contents
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_pull_without_upstream_warns(git_sandbox, git_repo_with_commit, git_daemon):
|
||||
repo_path = git_repo_with_commit
|
||||
remote_url = git_daemon["remote_url"]
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
|
||||
with pytest.raises(GitUpstreamException) as exc:
|
||||
git_sandbox.git.pull(repo_path)
|
||||
|
||||
assert "no upstream branch is configured" in str(exc.value).lower()
|
||||
@@ -0,0 +1,48 @@
|
||||
from e2b.template.readycmd import (
|
||||
wait_for_file,
|
||||
wait_for_port,
|
||||
wait_for_process,
|
||||
wait_for_timeout,
|
||||
wait_for_url,
|
||||
)
|
||||
|
||||
|
||||
def test_wait_for_port_matches_the_exact_listening_port():
|
||||
cmd = wait_for_port(80).get_cmd()
|
||||
assert cmd == '[ -n "$(ss -Htuln sport = :80)" ]'
|
||||
|
||||
|
||||
def test_wait_for_url_quotes_the_url():
|
||||
cmd = wait_for_url("http://localhost:3000/health?ready=1&x=y").get_cmd()
|
||||
assert cmd == (
|
||||
'curl -s -o /dev/null -w "%{http_code}" '
|
||||
"'http://localhost:3000/health?ready=1&x=y' | grep -q \"200\""
|
||||
)
|
||||
|
||||
|
||||
def test_wait_for_url_keeps_simple_urls_unquoted():
|
||||
cmd = wait_for_url("http://localhost:3000/health").get_cmd()
|
||||
assert cmd == (
|
||||
'curl -s -o /dev/null -w "%{http_code}" '
|
||||
'http://localhost:3000/health | grep -q "200"'
|
||||
)
|
||||
|
||||
|
||||
def test_wait_for_process_quotes_the_process_name():
|
||||
cmd = wait_for_process("my daemon").get_cmd()
|
||||
assert cmd == "pgrep 'my daemon' > /dev/null"
|
||||
|
||||
|
||||
def test_wait_for_file_quotes_the_filename():
|
||||
cmd = wait_for_file("/tmp/ready file").get_cmd()
|
||||
assert cmd == "[ -f '/tmp/ready file' ]"
|
||||
|
||||
|
||||
def test_wait_for_file_keeps_simple_paths_unquoted():
|
||||
cmd = wait_for_file("/tmp/ready").get_cmd()
|
||||
assert cmd == "[ -f /tmp/ready ]"
|
||||
|
||||
|
||||
def test_wait_for_timeout_converts_milliseconds_to_seconds():
|
||||
assert wait_for_timeout(5000).get_cmd() == "sleep 5"
|
||||
assert wait_for_timeout(100).get_cmd() == "sleep 1"
|
||||
@@ -0,0 +1,334 @@
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
from e2b.template.utils import get_all_files_in_path
|
||||
|
||||
|
||||
class TestGetAllFilesInPath:
|
||||
@pytest.fixture
|
||||
def test_dir(self):
|
||||
"""Create a temporary directory for testing."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
yield tmpdir
|
||||
|
||||
def test_should_return_files_matching_simple_pattern(self, test_dir):
|
||||
"""Test that function returns files matching a simple pattern."""
|
||||
# Create test files
|
||||
with open(os.path.join(test_dir, "file1.txt"), "w") as f:
|
||||
f.write("content1")
|
||||
with open(os.path.join(test_dir, "file2.txt"), "w") as f:
|
||||
f.write("content2")
|
||||
with open(os.path.join(test_dir, "file3.js"), "w") as f:
|
||||
f.write("content3")
|
||||
|
||||
files = get_all_files_in_path("*.txt", test_dir, [])
|
||||
|
||||
assert len(files) == 2
|
||||
assert any("file1.txt" in f for f in files)
|
||||
assert any("file2.txt" in f for f in files)
|
||||
assert not any("file3.js" in f for f in files)
|
||||
|
||||
def test_should_handle_directory_patterns_recursively(self, test_dir):
|
||||
"""Test that function handles directory patterns recursively."""
|
||||
# Create nested directory structure
|
||||
os.makedirs(os.path.join(test_dir, "src", "components"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "utils"), exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(os.path.join(test_dir, "src", "components", "Button.tsx"), "w") as f:
|
||||
f.write("button content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.ts"), "w") as f:
|
||||
f.write("helper content")
|
||||
with open(os.path.join(test_dir, "README.md"), "w") as f:
|
||||
f.write("readme content")
|
||||
|
||||
files = get_all_files_in_path("src", test_dir, [])
|
||||
|
||||
assert len(files) == 6 # 3 files + 3 directories (src, components, utils)
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any("Button.tsx" in f for f in files)
|
||||
assert any("helper.ts" in f for f in files)
|
||||
assert not any("README.md" in f for f in files)
|
||||
|
||||
def test_should_respect_ignore_patterns(self, test_dir):
|
||||
"""Test that function respects ignore patterns."""
|
||||
# Create test files
|
||||
with open(os.path.join(test_dir, "file1.txt"), "w") as f:
|
||||
f.write("content1")
|
||||
with open(os.path.join(test_dir, "file2.txt"), "w") as f:
|
||||
f.write("content2")
|
||||
with open(os.path.join(test_dir, "temp.txt"), "w") as f:
|
||||
f.write("temp content")
|
||||
with open(os.path.join(test_dir, "backup.txt"), "w") as f:
|
||||
f.write("backup content")
|
||||
|
||||
files = get_all_files_in_path("*.txt", test_dir, ["temp*", "backup*"])
|
||||
|
||||
assert len(files) == 2
|
||||
assert any("file1.txt" in f for f in files)
|
||||
assert any("file2.txt" in f for f in files)
|
||||
assert not any("temp.txt" in f for f in files)
|
||||
assert not any("backup.txt" in f for f in files)
|
||||
|
||||
def test_should_handle_complex_ignore_patterns(self, test_dir):
|
||||
"""Test that function handles complex ignore patterns."""
|
||||
# Create nested structure with various file types
|
||||
os.makedirs(os.path.join(test_dir, "src", "components"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "utils"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "tests"), exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(os.path.join(test_dir, "src", "components", "Button.tsx"), "w") as f:
|
||||
f.write("button content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.ts"), "w") as f:
|
||||
f.write("helper content")
|
||||
with open(os.path.join(test_dir, "tests", "test.spec.ts"), "w") as f:
|
||||
f.write("test content")
|
||||
with open(
|
||||
os.path.join(test_dir, "src", "components", "Button.test.tsx"), "w"
|
||||
) as f:
|
||||
f.write("test content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.spec.ts"), "w") as f:
|
||||
f.write("spec content")
|
||||
|
||||
files = get_all_files_in_path("src", test_dir, ["**/*.test.*", "**/*.spec.*"])
|
||||
|
||||
assert len(files) == 6 # 3 files + 3 directories (src, components, utils)
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any("Button.tsx" in f for f in files)
|
||||
assert any("helper.ts" in f for f in files)
|
||||
assert not any("Button.test.tsx" in f for f in files)
|
||||
assert not any("helper.spec.ts" in f for f in files)
|
||||
|
||||
def test_should_handle_empty_directories(self, test_dir):
|
||||
"""Test that function handles empty directories."""
|
||||
os.makedirs(os.path.join(test_dir, "empty"), exist_ok=True)
|
||||
with open(os.path.join(test_dir, "file.txt"), "w") as f:
|
||||
f.write("content")
|
||||
|
||||
files = get_all_files_in_path("empty", test_dir, [])
|
||||
|
||||
assert len(files) == 1
|
||||
|
||||
def test_should_handle_mixed_files_and_directories(self, test_dir):
|
||||
"""Test that function handles mixed files and directories."""
|
||||
# Create a mix of files and directories
|
||||
with open(os.path.join(test_dir, "file1.txt"), "w") as f:
|
||||
f.write("content1")
|
||||
os.makedirs(os.path.join(test_dir, "dir1"), exist_ok=True)
|
||||
with open(os.path.join(test_dir, "dir1", "file2.txt"), "w") as f:
|
||||
f.write("content2")
|
||||
with open(os.path.join(test_dir, "file3.txt"), "w") as f:
|
||||
f.write("content3")
|
||||
|
||||
files = get_all_files_in_path("*", test_dir, [])
|
||||
|
||||
assert len(files) == 4
|
||||
assert any("file1.txt" in f for f in files)
|
||||
assert any("file2.txt" in f for f in files)
|
||||
assert any("file3.txt" in f for f in files)
|
||||
|
||||
def test_should_handle_glob_patterns_with_subdirectories(self, test_dir):
|
||||
"""Test that function handles glob patterns with subdirectories."""
|
||||
# Create nested structure
|
||||
os.makedirs(os.path.join(test_dir, "src", "components"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "utils"), exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(os.path.join(test_dir, "src", "components", "Button.tsx"), "w") as f:
|
||||
f.write("button content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.ts"), "w") as f:
|
||||
f.write("helper content")
|
||||
with open(os.path.join(test_dir, "src", "components", "Button.css"), "w") as f:
|
||||
f.write("css content")
|
||||
|
||||
files = get_all_files_in_path("src/**/*", test_dir, [])
|
||||
|
||||
assert len(files) == 6
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any("Button.tsx" in f for f in files)
|
||||
assert any("helper.ts" in f for f in files)
|
||||
assert any("Button.css" in f for f in files)
|
||||
|
||||
def test_should_handle_specific_file_extensions(self, test_dir):
|
||||
"""Test that function handles specific file extensions."""
|
||||
with open(os.path.join(test_dir, "file1.ts"), "w") as f:
|
||||
f.write("ts content")
|
||||
with open(os.path.join(test_dir, "file2.js"), "w") as f:
|
||||
f.write("js content")
|
||||
with open(os.path.join(test_dir, "file3.tsx"), "w") as f:
|
||||
f.write("tsx content")
|
||||
with open(os.path.join(test_dir, "file4.css"), "w") as f:
|
||||
f.write("css content")
|
||||
|
||||
files = get_all_files_in_path("*.ts", test_dir, [])
|
||||
|
||||
assert len(files) == 1
|
||||
assert any("file1.ts" in f for f in files)
|
||||
|
||||
def test_should_return_sorted_files(self, test_dir):
|
||||
"""Test that function returns sorted files."""
|
||||
with open(os.path.join(test_dir, "zebra.txt"), "w") as f:
|
||||
f.write("z content")
|
||||
with open(os.path.join(test_dir, "apple.txt"), "w") as f:
|
||||
f.write("a content")
|
||||
with open(os.path.join(test_dir, "banana.txt"), "w") as f:
|
||||
f.write("b content")
|
||||
|
||||
files = get_all_files_in_path("*.txt", test_dir, [])
|
||||
|
||||
assert len(files) == 3
|
||||
assert "apple.txt" in files[0]
|
||||
assert "banana.txt" in files[1]
|
||||
assert "zebra.txt" in files[2]
|
||||
|
||||
def test_should_handle_no_matching_files(self, test_dir):
|
||||
"""Test that function handles no matching files."""
|
||||
with open(os.path.join(test_dir, "file.txt"), "w") as f:
|
||||
f.write("content")
|
||||
|
||||
files = get_all_files_in_path("*.js", test_dir, [])
|
||||
|
||||
assert len(files) == 0
|
||||
|
||||
def test_should_handle_complex_ignore_patterns_with_directories(self, test_dir):
|
||||
"""Test that function handles complex ignore patterns with directories."""
|
||||
# Create a complex structure
|
||||
os.makedirs(os.path.join(test_dir, "src", "components"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "utils"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "tests"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "dist"), exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(os.path.join(test_dir, "src", "components", "Button.tsx"), "w") as f:
|
||||
f.write("button content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.ts"), "w") as f:
|
||||
f.write("helper content")
|
||||
with open(os.path.join(test_dir, "src", "tests", "test.spec.ts"), "w") as f:
|
||||
f.write("test content")
|
||||
with open(os.path.join(test_dir, "dist", "bundle.js"), "w") as f:
|
||||
f.write("bundle content")
|
||||
with open(os.path.join(test_dir, "README.md"), "w") as f:
|
||||
f.write("readme content")
|
||||
|
||||
files = get_all_files_in_path("src", test_dir, ["**/tests/**", "**/*.spec.*"])
|
||||
|
||||
assert len(files) == 6 # 3 files + 3 directories (src, components, utils)
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any("Button.tsx" in f for f in files)
|
||||
assert any("helper.ts" in f for f in files)
|
||||
assert not any("test.spec.ts" in f for f in files)
|
||||
|
||||
def test_should_include_dotfiles(self, test_dir):
|
||||
"""Test that function includes files starting with a dot."""
|
||||
with open(os.path.join(test_dir, "file.txt"), "w") as f:
|
||||
f.write("content")
|
||||
with open(os.path.join(test_dir, ".env"), "w") as f:
|
||||
f.write("SECRET=123")
|
||||
with open(os.path.join(test_dir, ".gitignore"), "w") as f:
|
||||
f.write("node_modules")
|
||||
|
||||
files = get_all_files_in_path("*", test_dir, [])
|
||||
|
||||
assert len(files) == 3
|
||||
assert any(".env" in f for f in files)
|
||||
assert any(".gitignore" in f for f in files)
|
||||
assert any("file.txt" in f for f in files)
|
||||
|
||||
def test_should_include_dotfiles_in_subdirectories(self, test_dir):
|
||||
"""Test that function includes dotfiles inside subdirectories."""
|
||||
os.makedirs(os.path.join(test_dir, "src"), exist_ok=True)
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("content")
|
||||
with open(os.path.join(test_dir, "src", ".env.local"), "w") as f:
|
||||
f.write("SECRET=123")
|
||||
|
||||
files = get_all_files_in_path("src", test_dir, [])
|
||||
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any(".env.local" in f for f in files)
|
||||
|
||||
def test_should_include_dotdirectories_and_their_contents(self, test_dir):
|
||||
"""Test that function includes dot-prefixed directories and their contents."""
|
||||
os.makedirs(os.path.join(test_dir, ".hidden"), exist_ok=True)
|
||||
with open(os.path.join(test_dir, ".hidden", "config.json"), "w") as f:
|
||||
f.write("{}")
|
||||
with open(os.path.join(test_dir, "visible.txt"), "w") as f:
|
||||
f.write("content")
|
||||
|
||||
files = get_all_files_in_path("*", test_dir, [])
|
||||
|
||||
assert any(".hidden" in f for f in files)
|
||||
assert any("config.json" in f for f in files)
|
||||
assert any("visible.txt" in f for f in files)
|
||||
|
||||
def test_should_respect_ignore_patterns_for_dotfiles(self, test_dir):
|
||||
"""Test that dotfiles can be excluded via ignore patterns."""
|
||||
with open(os.path.join(test_dir, ".env"), "w") as f:
|
||||
f.write("SECRET=123")
|
||||
with open(os.path.join(test_dir, ".gitignore"), "w") as f:
|
||||
f.write("node_modules")
|
||||
with open(os.path.join(test_dir, "file.txt"), "w") as f:
|
||||
f.write("content")
|
||||
|
||||
files = get_all_files_in_path("*", test_dir, [".env"])
|
||||
|
||||
assert len(files) == 2
|
||||
assert not any(f.endswith(".env") for f in files)
|
||||
assert any(".gitignore" in f for f in files)
|
||||
assert any("file.txt" in f for f in files)
|
||||
|
||||
def test_should_handle_symlinks(self, test_dir):
|
||||
"""Test that function handles symbolic links."""
|
||||
# Create a file and a symlink to it
|
||||
with open(os.path.join(test_dir, "original.txt"), "w") as f:
|
||||
f.write("original content")
|
||||
|
||||
# Create symlink (only on Unix-like systems)
|
||||
if hasattr(os, "symlink"):
|
||||
os.symlink("original.txt", os.path.join(test_dir, "link.txt"))
|
||||
|
||||
files = get_all_files_in_path("*.txt", test_dir, [])
|
||||
|
||||
assert len(files) == 2
|
||||
assert any("original.txt" in f for f in files)
|
||||
assert any("link.txt" in f for f in files)
|
||||
|
||||
def test_should_handle_nested_ignore_patterns(self, test_dir):
|
||||
"""Test that function handles nested ignore patterns."""
|
||||
# Create nested structure
|
||||
os.makedirs(os.path.join(test_dir, "src", "components", "ui"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "components", "forms"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "utils"), exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(
|
||||
os.path.join(test_dir, "src", "components", "ui", "Button.tsx"), "w"
|
||||
) as f:
|
||||
f.write("button content")
|
||||
with open(
|
||||
os.path.join(test_dir, "src", "components", "forms", "Input.tsx"), "w"
|
||||
) as f:
|
||||
f.write("input content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.ts"), "w") as f:
|
||||
f.write("helper content")
|
||||
with open(
|
||||
os.path.join(test_dir, "src", "components", "ui", "Button.test.tsx"), "w"
|
||||
) as f:
|
||||
f.write("test content")
|
||||
|
||||
files = get_all_files_in_path("src", test_dir, ["**/ui/**"])
|
||||
|
||||
assert (
|
||||
len(files) == 7
|
||||
) # 3 files + 4 directories (src, components, forms, utils)
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any("Input.tsx" in f for f in files)
|
||||
assert any("helper.ts" in f for f in files)
|
||||
assert not any("Button.tsx" in f for f in files)
|
||||
assert not any("Button.test.tsx" in f for f in files)
|
||||
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
from e2b.template.utils import get_caller_directory
|
||||
|
||||
|
||||
def test_get_caller_directory():
|
||||
assert get_caller_directory(1) == os.path.dirname(__file__)
|
||||
@@ -0,0 +1,45 @@
|
||||
import pytest
|
||||
|
||||
from e2b.template.utils import normalize_build_arguments
|
||||
from e2b.exceptions import TemplateException
|
||||
|
||||
|
||||
def test_handles_string_name():
|
||||
result = normalize_build_arguments(name="my-template:v1.0")
|
||||
assert result == "my-template:v1.0"
|
||||
|
||||
|
||||
def test_handles_name_without_tag():
|
||||
result = normalize_build_arguments(name="my-template")
|
||||
assert result == "my-template"
|
||||
|
||||
|
||||
def test_handles_legacy_alias():
|
||||
result = normalize_build_arguments(alias="my-template")
|
||||
assert result == "my-template"
|
||||
|
||||
|
||||
def test_name_takes_precedence_over_alias():
|
||||
# When both are provided, name should be used
|
||||
result = normalize_build_arguments(name="from-name", alias="from-alias")
|
||||
assert result == "from-name"
|
||||
|
||||
|
||||
def test_throws_for_empty_name():
|
||||
with pytest.raises(TemplateException, match="Name must be provided"):
|
||||
normalize_build_arguments(name="")
|
||||
|
||||
|
||||
def test_throws_for_empty_alias():
|
||||
with pytest.raises(TemplateException, match="Name must be provided"):
|
||||
normalize_build_arguments(alias="")
|
||||
|
||||
|
||||
def test_throws_for_missing_name_and_alias():
|
||||
with pytest.raises(TemplateException, match="Name must be provided"):
|
||||
normalize_build_arguments()
|
||||
|
||||
|
||||
def test_throws_for_none_values():
|
||||
with pytest.raises(TemplateException, match="Name must be provided"):
|
||||
normalize_build_arguments(name=None, alias=None)
|
||||
@@ -0,0 +1,29 @@
|
||||
from e2b.template.utils import strip_ansi_escape_codes
|
||||
|
||||
|
||||
def test_strips_basic_sgr_color():
|
||||
assert strip_ansi_escape_codes("\x1b[31mred\x1b[0m") == "red"
|
||||
|
||||
|
||||
def test_strips_semicolon_separated_params():
|
||||
assert strip_ansi_escape_codes("\x1b[1;31;42mhi\x1b[0m") == "hi"
|
||||
|
||||
|
||||
def test_strips_semicolon_256_color():
|
||||
assert strip_ansi_escape_codes("\x1b[38;5;82mX\x1b[0m") == "X"
|
||||
|
||||
|
||||
def test_strips_colon_256_color():
|
||||
assert strip_ansi_escape_codes("\x1b[38:5:82mX\x1b[0m") == "X"
|
||||
|
||||
|
||||
def test_strips_colon_truecolor():
|
||||
assert strip_ansi_escape_codes("\x1b[38:2::255:0:0mRED\x1b[0m") == "RED"
|
||||
|
||||
|
||||
def test_strips_colon_curly_underline():
|
||||
assert strip_ansi_escape_codes("\x1b[4:3mX\x1b[0m") == "X"
|
||||
|
||||
|
||||
def test_leaves_plain_text_unchanged():
|
||||
assert strip_ansi_escape_codes("no escape codes here") == "no escape codes here"
|
||||
@@ -0,0 +1,172 @@
|
||||
import os
|
||||
import tempfile
|
||||
import tarfile
|
||||
import pytest
|
||||
from typing import IO
|
||||
from e2b.template.utils import tar_file_stream
|
||||
|
||||
|
||||
class TestTarFileStream:
|
||||
@pytest.fixture
|
||||
def test_dir(self):
|
||||
"""Create a temporary directory for testing."""
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
yield tmpdir.name
|
||||
tmpdir.cleanup()
|
||||
|
||||
def _extract_tar_contents(self, tar_buffer: IO[bytes]) -> dict:
|
||||
"""Extract tar contents into a dictionary mapping paths to file contents."""
|
||||
tar_buffer.seek(0)
|
||||
contents = {}
|
||||
# "r:*" auto-detects compressed vs. uncompressed archives
|
||||
with tarfile.open(fileobj=tar_buffer, mode="r:*") as tar:
|
||||
for member in tar.getmembers():
|
||||
if member.isfile():
|
||||
file_obj = tar.extractfile(member)
|
||||
if file_obj:
|
||||
contents[member.name] = file_obj.read()
|
||||
elif member.isdir():
|
||||
contents[member.name] = None # Mark as directory
|
||||
return contents
|
||||
|
||||
def test_should_create_tar_with_simple_files(self, test_dir):
|
||||
"""Test that function creates tar with simple files."""
|
||||
# Create test files
|
||||
file1_path = os.path.join(test_dir, "file1.txt")
|
||||
file2_path = os.path.join(test_dir, "file2.txt")
|
||||
|
||||
with open(file1_path, "w") as f:
|
||||
f.write("content1")
|
||||
with open(file2_path, "w") as f:
|
||||
f.write("content2")
|
||||
|
||||
tar_buffer = tar_file_stream("*.txt", test_dir, [], False, True)
|
||||
contents = self._extract_tar_contents(tar_buffer)
|
||||
|
||||
assert len(contents) == 2
|
||||
assert "file1.txt" in contents
|
||||
assert "file2.txt" in contents
|
||||
assert contents["file1.txt"] == b"content1"
|
||||
assert contents["file2.txt"] == b"content2"
|
||||
|
||||
def test_should_create_uncompressed_tar_when_gzip_disabled(self, test_dir):
|
||||
"""Test that function creates an uncompressed tar when gzip=False."""
|
||||
file1_path = os.path.join(test_dir, "file1.txt")
|
||||
file2_path = os.path.join(test_dir, "file2.txt")
|
||||
|
||||
with open(file1_path, "w") as f:
|
||||
f.write("content1")
|
||||
with open(file2_path, "w") as f:
|
||||
f.write("content2")
|
||||
|
||||
tar_buffer = tar_file_stream("*.txt", test_dir, [], False, False)
|
||||
|
||||
# gzip streams start with the magic bytes 0x1f 0x8b — a plain tar must not
|
||||
tar_buffer.seek(0)
|
||||
assert tar_buffer.read(2) != b"\x1f\x8b"
|
||||
|
||||
# The archive must still be readable and contain the original files
|
||||
contents = self._extract_tar_contents(tar_buffer)
|
||||
assert contents["file1.txt"] == b"content1"
|
||||
assert contents["file2.txt"] == b"content2"
|
||||
|
||||
def test_should_respect_ignore_patterns(self, test_dir):
|
||||
"""Test that function respects ignore patterns."""
|
||||
# Create test files
|
||||
with open(os.path.join(test_dir, "file1.txt"), "w") as f:
|
||||
f.write("content1")
|
||||
with open(os.path.join(test_dir, "file2.txt"), "w") as f:
|
||||
f.write("content2")
|
||||
with open(os.path.join(test_dir, "temp.txt"), "w") as f:
|
||||
f.write("temp content")
|
||||
with open(os.path.join(test_dir, "backup.txt"), "w") as f:
|
||||
f.write("backup content")
|
||||
|
||||
tar_buffer = tar_file_stream(
|
||||
"*.txt", test_dir, ["temp*", "backup*"], False, True
|
||||
)
|
||||
contents = self._extract_tar_contents(tar_buffer)
|
||||
|
||||
assert len(contents) == 2
|
||||
assert "file1.txt" in contents
|
||||
assert "file2.txt" in contents
|
||||
assert contents["file1.txt"] == b"content1"
|
||||
assert contents["file2.txt"] == b"content2"
|
||||
assert "temp.txt" not in contents
|
||||
assert "backup.txt" not in contents
|
||||
|
||||
def test_should_handle_nested_files(self, test_dir):
|
||||
"""Test that function handles nested directory structures."""
|
||||
# Create nested directory structure
|
||||
nested_dir = os.path.join(test_dir, "src", "components")
|
||||
os.makedirs(nested_dir, exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(os.path.join(nested_dir, "Button.tsx"), "w") as f:
|
||||
f.write("button content")
|
||||
|
||||
tar_buffer = tar_file_stream("src", test_dir, [], False, True)
|
||||
contents = self._extract_tar_contents(tar_buffer)
|
||||
|
||||
# Should include the directory and files
|
||||
assert "src/index.ts" in contents
|
||||
assert "src/components/Button.tsx" in contents
|
||||
|
||||
def test_should_resolve_symlinks_when_enabled(self, test_dir):
|
||||
"""Test that function resolves symlinks when resolve_symlinks=True."""
|
||||
if not hasattr(os, "symlink"):
|
||||
pytest.skip("Symlinks not supported on this platform")
|
||||
|
||||
# Create original file
|
||||
original_path = os.path.join(test_dir, "original.txt")
|
||||
with open(original_path, "w") as f:
|
||||
f.write("original content")
|
||||
|
||||
# Create symlink
|
||||
symlink_path = os.path.join(test_dir, "link.txt")
|
||||
os.symlink("original.txt", symlink_path)
|
||||
|
||||
# Test with resolve_symlinks=True
|
||||
tar_buffer = tar_file_stream("*.txt", test_dir, [], True, True)
|
||||
contents = self._extract_tar_contents(tar_buffer)
|
||||
|
||||
# Both files should be in tar
|
||||
assert "original.txt" in contents
|
||||
assert "link.txt" in contents
|
||||
# Symlink should be resolved (contain actual content, not link)
|
||||
assert contents["original.txt"] == b"original content"
|
||||
assert contents["link.txt"] == b"original content"
|
||||
|
||||
def test_should_preserve_symlinks_when_disabled(self, test_dir):
|
||||
"""Test that function preserves symlinks when resolve_symlinks=False."""
|
||||
if not hasattr(os, "symlink"):
|
||||
pytest.skip("Symlinks not supported on this platform")
|
||||
|
||||
# Create original file
|
||||
original_path = os.path.join(test_dir, "original.txt")
|
||||
with open(original_path, "w") as f:
|
||||
f.write("original content")
|
||||
|
||||
# Create symlink
|
||||
symlink_path = os.path.join(test_dir, "link.txt")
|
||||
os.symlink("original.txt", symlink_path)
|
||||
|
||||
# Test with resolve_symlinks=False
|
||||
tar_buffer = tar_file_stream("*.txt", test_dir, [], False, True)
|
||||
tar_buffer.seek(0)
|
||||
|
||||
with tarfile.open(fileobj=tar_buffer, mode="r:gz") as tar:
|
||||
members = {m.name: m for m in tar.getmembers()}
|
||||
|
||||
# Both files should be in tar
|
||||
assert "original.txt" in members
|
||||
assert "link.txt" in members
|
||||
|
||||
# Original should be a regular file
|
||||
assert members["original.txt"].isfile()
|
||||
assert not members["original.txt"].issym()
|
||||
|
||||
# Link should be a symlink
|
||||
assert members["link.txt"].issym()
|
||||
assert members["link.txt"].linkname == "original.txt"
|
||||
@@ -0,0 +1,106 @@
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from e2b.template.utils import validate_relative_path
|
||||
from e2b.exceptions import TemplateException
|
||||
|
||||
is_windows = sys.platform == "win32"
|
||||
|
||||
|
||||
class TestValidateRelativePathValid:
|
||||
"""Test cases for valid paths."""
|
||||
|
||||
def test_accepts_simple_relative_path(self):
|
||||
validate_relative_path("foo", None)
|
||||
|
||||
def test_accepts_nested_relative_path(self):
|
||||
validate_relative_path("foo/bar", None)
|
||||
|
||||
def test_accepts_path_with_dot_prefix(self):
|
||||
validate_relative_path("./foo", None)
|
||||
|
||||
def test_accepts_nested_path_with_dot_prefix(self):
|
||||
validate_relative_path("./foo/bar", None)
|
||||
|
||||
def test_accepts_internal_parent_ref_within_context(self):
|
||||
validate_relative_path("foo/../bar", None)
|
||||
|
||||
def test_accepts_current_directory(self):
|
||||
validate_relative_path(".", None)
|
||||
|
||||
def test_accepts_glob_patterns(self):
|
||||
validate_relative_path("*.txt", None)
|
||||
validate_relative_path("**/*.ts", None)
|
||||
validate_relative_path("src/**/*", None)
|
||||
|
||||
def test_accepts_filenames_starting_with_double_dots(self):
|
||||
validate_relative_path("..myconfig", None)
|
||||
validate_relative_path("..cache", None)
|
||||
validate_relative_path("...something", None)
|
||||
validate_relative_path("foo/..myconfig", None)
|
||||
|
||||
|
||||
class TestValidateRelativePathInvalidAbsolute:
|
||||
"""Test cases for invalid absolute paths."""
|
||||
|
||||
def test_rejects_unix_absolute_path(self):
|
||||
with pytest.raises(TemplateException) as excinfo:
|
||||
validate_relative_path("/absolute/path", None)
|
||||
assert "absolute paths are not allowed" in str(excinfo.value)
|
||||
|
||||
def test_rejects_root_path(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("/", None)
|
||||
|
||||
@pytest.mark.skipif(not is_windows, reason="Windows path test only runs on Windows")
|
||||
def test_rejects_windows_drive_letter_path(self):
|
||||
with pytest.raises(TemplateException) as excinfo:
|
||||
validate_relative_path("C:\\Windows\\System32", None)
|
||||
assert "absolute paths are not allowed" in str(excinfo.value)
|
||||
|
||||
|
||||
class TestValidateRelativePathInvalidEscape:
|
||||
"""Test cases for paths that escape the context directory."""
|
||||
|
||||
def test_rejects_simple_parent_directory_escape(self):
|
||||
with pytest.raises(TemplateException) as excinfo:
|
||||
validate_relative_path("../foo", None)
|
||||
assert "path escapes the context directory" in str(excinfo.value)
|
||||
|
||||
def test_rejects_double_parent_directory_escape(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("../../foo", None)
|
||||
|
||||
def test_rejects_nested_parent_refs_escape(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("foo/../../bar", None)
|
||||
|
||||
def test_rejects_dot_prefix_escape(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("./foo/../../../bar", None)
|
||||
|
||||
def test_rejects_just_parent_directory(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("..", None)
|
||||
|
||||
def test_rejects_current_directory_followed_by_parent(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("./..", None)
|
||||
|
||||
def test_rejects_deeply_nested_escape(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("a/b/c/../../../../escape", None)
|
||||
|
||||
|
||||
class TestValidateRelativePathErrorMessages:
|
||||
"""Test cases for error message content."""
|
||||
|
||||
def test_absolute_path_error_includes_path(self):
|
||||
with pytest.raises(TemplateException) as excinfo:
|
||||
validate_relative_path("/etc/passwd", None)
|
||||
assert "/etc/passwd" in str(excinfo.value)
|
||||
|
||||
def test_escape_path_error_includes_path(self):
|
||||
with pytest.raises(TemplateException) as excinfo:
|
||||
validate_relative_path("../secret", None)
|
||||
assert "../secret" in str(excinfo.value)
|
||||
Reference in New Issue
Block a user