Skip to content

Commit

Permalink
Add flag to choose template (#269)
Browse files Browse the repository at this point in the history
* Add flag to choose template

This change allows providing a custom template arg flag -t/--template:
```
sinol-make init foo -t [repo link or disk path] [optional subdir]
```
It defaults to the upstream repository and example_package subdir.

Changed the example_package id pattern from `abc` to `__ID__`.
All filenames and file contents are now substituted.

Changed the positional output directory argument to `-o/--output` flag
to help avoid accidents.

Added result directory cleanup on failure.

* Test quick fix #1

* Always cleanup tmpdir

* Add other git url schemes

* One more used_tmpdir tweak

* Pacify tests #2

* Leave old 'abc' template string for now

* Test without connecting to github

Try accessing the local git repo in the project root first when cloning
example_package. Only if that doesn't work, fall back to online github.

Also adds some extra local path tests to init/test_unit.
Also removes one unnecessary os.getcwd() call.
Also removes the old "abc" template string hack since we test the
current template now.

* Fall back on copying local directory instead of online github
  • Loading branch information
j4b6ski authored Dec 10, 2024
1 parent 3322119 commit 8cf5344
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 41 deletions.
12 changes: 6 additions & 6 deletions example_package/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,20 @@ override_limits:
# Each language can have different extra arguments.

# extra_compilation_args:
# cpp: 'abclib.cpp'
# cpp: '__ID__lib.cpp'

# The arguments can also be in an array:

# extra_compilation_args:
# cpp:
# - 'abclib.cpp'
# - 'abclib2.cpp'
# - '__ID__lib.cpp'
# - '__ID__lib2.cpp'

# Additional files used in compilation can be defined in `extra_compilation_files` key.
# They are copied to the directory where the source code is compiled.
# All languages have the same additional files.

# extra_compilation_files: ['abclib.cpp', 'abclib.py']
# extra_compilation_files: ['__ID__lib.cpp', '__ID__lib.py']


### Keys used by sinol-make:
Expand All @@ -65,7 +65,7 @@ override_limits:
# The names of files in `prog/`, `doc/`, `in/` and `out/` directories have to start with this task id.
# This key is only used by `sinol-make`: running `sinol-make export` creates
# an archive with the proper name, which sio2 uses as the task id.
sinol_task_id: abc
sinol_task_id: __ID__

# sinol-make can behave differently depending on the value of `sinol_contest_type` key.
# Mainly, it affects how points are calculated.
Expand All @@ -76,7 +76,7 @@ sinol_contest_type: oi
# You can specify which tests are static (handwritten). This allows sinol-make to differentiate between
# old and handwritten tests. If this key is not present old tests won't be removed.
# This key is optional and should be a list of tests.
sinol_static_tests: ["abc0.in", "abc0a.in"]
sinol_static_tests: ["__ID__0.in", "__ID__0a.in"]

# sinol-make can check if the solutions run as expected when using `run` command.
# Key `sinol_expected_scores` defines expected scores for each solution on each tests.
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using namespace std;

// Change this function to generate one test for stresstesting.
// The script prog/abcingen.sh in 10 seconds generates
// The script prog/__ID__ingen.sh in 10 seconds generates
// as much tests as possible and compares the outputs
// of the model solution and brute solution.
// The tests shouldn't be very big, but should be able to cover edge cases.
Expand All @@ -12,7 +12,7 @@ void generate_one_stresstest(oi::Random &rng) {
}

// Change this function to create a test with the given name.
// The lists of tests to generate needs to be written in prog/abcingen.sh
// The lists of tests to generate needs to be written in prog/__ID__ingen.sh
void generate_proper_test(string test_name, oi::Random &rng) {
if (test_name == "0a")
cout << "0 1" << endl;
Expand All @@ -34,7 +34,7 @@ int main(int argc, char *argv[]) {
return 0;
}
if (argc != 2) {
cerr << "Run prog/abcingen.sh to stresstest and create proper tests." << endl;
cerr << "Run prog/__ID__ingen.sh to stresstest and create proper tests." << endl;
exit(1);
}
string test_name = argv[1];
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
77 changes: 51 additions & 26 deletions src/sinol_make/commands/init/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class Command(BaseCommand):
Class for "init"
"""

TEMPLATE_ID='__ID__'
DEFAULT_TEMPLATE = 'https://github.com/sio2project/sinol-make.git'
DEFAULT_SUBDIR = 'example_package'

def get_name(self):
return "init"

Expand All @@ -23,21 +27,32 @@ def configure_subparser(self, subparser: argparse.ArgumentParser):
description='Create package from predefined template with given id.'
)
parser.add_argument('task_id', type=str, help='id of the task to create')
parser.add_argument('directory', type=str, nargs='?',
parser.add_argument('-o', '--output', type=str,
help='destination directory to copy the template into, defaults to task_id')
parser.add_argument('-f', '--force', action='store_true',
help='overwrite files in destination directory if they already exist')
parser.add_argument('-t', '--template', nargs='+', default=[self.DEFAULT_TEMPLATE, self.DEFAULT_SUBDIR],
help='specify template repository or directory, optionally subdirectory after space'
f' (default: {self.DEFAULT_TEMPLATE} {self.DEFAULT_SUBDIR})')
parser.add_argument('-v', '--verbose', action='store_true')
return parser

def download_template(self):
repo = 'https://github.com/sio2project/sinol-make.git'
package_dir = 'sinol-make/example_package'
self.used_tmpdir = tempfile.TemporaryDirectory()
tmp_dir = self.used_tmpdir.name
ret = subprocess.run(['git', 'clone', '-q', '--depth', '1', repo], cwd=tmp_dir)
if ret.returncode != 0:
util.exit_with_error("Could not access repository. Please try again.")
path = os.path.join(tmp_dir, package_dir)
def download_template(self, tmpdir, template_paths = [DEFAULT_TEMPLATE, DEFAULT_SUBDIR], verbose = False):
template = template_paths[0]
subdir = template_paths[1] if len(template_paths) > 1 else ''

is_url = template.startswith(('http://', 'https://', 'ssh://', 'git@', 'file://'))
print(('Cloning' if is_url else 'Copying') + ' template ' +
(f'{subdir} from {template}' if subdir else f'{template}'))
if is_url:
ret = subprocess.run(['git', 'clone', '-v' if verbose else '-q', '--depth', '1', template, tmpdir])
if ret.returncode != 0:
util.exit_with_error("Could not access repository. Please try again.")
path = os.path.join(tmpdir, subdir)
else:
path = os.path.join(tmpdir, 'template')
shutil.copytree(os.path.join(template, subdir), path)

if os.path.exists(os.path.join(path, '.git')):
shutil.rmtree(os.path.join(path, '.git'))
return path
Expand All @@ -55,22 +70,29 @@ def move_folder(self):
raise
for file in files:
dest_filename = file
if file[:3] == 'abc':
dest_filename = self.task_id + file[3:]
if file[:len(self.TEMPLATE_ID)] == self.TEMPLATE_ID:
dest_filename = self.task_id + file[len(self.TEMPLATE_ID):]
shutil.move(os.path.join(root, file), os.path.join(mapping[root], dest_filename))

def update_config(self):
with open(os.path.join(os.getcwd(), 'config.yml')) as config:
config_data = config.read()
config_data = config_data.replace('sinol_task_id: abc', f'sinol_task_id: {self.task_id}')
def update_task_id(self):
for root, dirs, files in os.walk(os.getcwd()):
for file in files:
path = os.path.join(os.getcwd(), root, file)
with open(path) as file:
try:
file_data = file.read()
except UnicodeDecodeError:
# ignore non-text files
continue
file_data = file_data.replace(self.TEMPLATE_ID, self.task_id)

with open(os.path.join(os.getcwd(), 'config.yml'), 'w') as config:
config.write(config_data)
with open(path, 'w') as file:
file.write(file_data)

def run(self, args: argparse.Namespace):
self.task_id = args.task_id
self.force = args.force
destination = args.directory or self.task_id
destination = args.output or self.task_id
if not os.path.isabs(destination):
destination = os.path.join(os.getcwd(), destination)
try:
Expand All @@ -80,13 +102,16 @@ def run(self, args: argparse.Namespace):
util.exit_with_error(f"Destination {destination} already exists. "
f"Provide a different task id or directory name, "
f"or use the --force flag to overwrite.")
os.chdir(destination)

self.template_dir = self.download_template()
with tempfile.TemporaryDirectory() as tmpdir:
try:
self.template_dir = self.download_template(tmpdir, args.template, args.verbose)

self.move_folder()
self.update_config()
os.chdir(destination)

self.used_tmpdir.cleanup()
self.move_folder()
self.update_task_id()

print(util.info(f'Successfully created task "{self.task_id}"'))
print(util.info(f'Successfully created task "{self.task_id}"'))
except:
shutil.rmtree(destination)
raise
19 changes: 16 additions & 3 deletions tests/commands/init/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,25 @@


@pytest.mark.parametrize("temp_workdir", [''], indirect=True)
def test_simple(capsys, temp_workdir):
def test_init_clones_default_template(capsys, request, temp_workdir):
"""
Test `init` command.
"""
parser = configure_parsers()
args = parser.parse_args(["init", "xyz"])
args = ["init", "xyz"]

# try to avoid connecting to github when cloning example_package
rootdir = str(request.config.rootdir)
git_dir = os.path.join(rootdir, '.git')
if os.path.exists(git_dir):
git_local_url = os.path.join('file://', rootdir)
args.extend(["-t", git_local_url, "example_package"])
else:
# copy from root directory instead of cloning
# if needed we could take a dependency on gitpython and mock up a repo
args.extend(["-t", rootdir, "example_package"])

args = parser.parse_args(args)
command = Command()
command.run(args)
out = capsys.readouterr().out
Expand All @@ -23,7 +36,7 @@ def test_simple(capsys, temp_workdir):

for file in expected_files:
assert os.path.isfile(os.path.join(os.getcwd(), file))

# Check if task id is correctly set
with open(os.path.join(os.getcwd(), 'config.yml')) as config_file:
config_file_data = config_file.read()
Expand Down
33 changes: 30 additions & 3 deletions tests/commands/init/test_unit.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
import os
import tempfile
import pytest

from sinol_make.commands.init import Command


def test_if_download_successful():
def copy_template(rootdir):
template_path = [rootdir, Command.DEFAULT_SUBDIR]
command = Command()
tmp_dir = command.download_template()
assert os.path.isfile(os.path.join(tmp_dir,'config.yml'))
with tempfile.TemporaryDirectory() as tmpdir:
tmp_dir = command.download_template(tmpdir, template_path)
assert os.path.isfile(os.path.join(tmp_dir, 'config.yml'))


def test_clones_default_template(request):
# try to avoid connecting to github when cloning example_package
rootdir = str(request.config.rootdir)
git_dir = os.path.join(rootdir, '.git')
if not os.path.exists(git_dir):
# if needed we could take a dependency on gitpython and mock up a repo
pytest.skip(f".git not found in rootdir {rootdir}")

git_local_url = os.path.join('file://', rootdir)
copy_template(git_local_url)


def test_copies_local_template_absolute_path(request):
rootdir_absolute = str(request.config.rootdir)
copy_template(rootdir_absolute)


def test_copies_local_template_relative_path(request):
os.chdir(os.path.join(request.config.rootdir, '..'))
rootdir_relative = os.path.relpath(request.config.rootdir, os.getcwd())
copy_template(rootdir_relative)

0 comments on commit 8cf5344

Please sign in to comment.