Repro cmd first unttest

This commit is contained in:
Dmitry Petrov
2017-04-03 03:56:08 -07:00
parent 2f8c2e9397
commit dccc15c0d6
7 changed files with 242 additions and 83 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ from dvc.utils import cached_property
class CmdBase(object): class CmdBase(object):
CONFIG = 'dvc.conf' CONFIG = 'dvc.conf'
def __init__(self, parse_config=True, git_obj=None, config_obj=None): def __init__(self, args=None, parse_config=True, git_obj=None, config_obj=None):
if git_obj: if git_obj:
self._git = git_obj self._git = git_obj
else: else:
@@ -29,7 +29,7 @@ class CmdBase(object):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
self.define_args(parser) self.define_args(parser)
self._args, self._args_unkn = parser.parse_known_args() self._args, self._args_unkn = parser.parse_known_args(args=args)
self._dvc_home = os.environ.get('DVC_HOME') self._dvc_home = os.environ.get('DVC_HOME')
+3 -3
View File
@@ -15,8 +15,8 @@ class DataRemoveError(DvcException):
class CmdDataRemove(CmdBase): class CmdDataRemove(CmdBase):
def __init__(self, parse_config=True, git_obj=None, config_obj=None): def __init__(self, args=None, parse_config=True, git_obj=None, config_obj=None):
super(CmdDataRemove, self).__init__(parse_config, git_obj, config_obj) super(CmdDataRemove, self).__init__(args, parse_config, git_obj, config_obj)
pass pass
def define_args(self, parser): def define_args(self, parser):
@@ -24,7 +24,7 @@ class CmdDataRemove(CmdBase):
parser.add_argument('target', metavar='', help='Target to remove - file or directory', nargs='*') parser.add_argument('target', metavar='', help='Target to remove - file or directory', nargs='*')
parser.add_argument('-r', '--recursive', action='store_true', help='Remove directory recursively') parser.add_argument('-r', '--recursive', action='store_true', help='Remove directory recursively')
parser.add_argument('-l', '--keep-in-cloud', action='store_false', default=False, parser.add_argument('-l', '--keep-in-cloud', action='store_true', default=False,
help='Do not remove data from cloud') help='Do not remove data from cloud')
parser.add_argument('-c', '--keep-in-cache', action='store_false', default=False, parser.add_argument('-c', '--keep-in-cache', action='store_false', default=False,
help='Do not remove data from cache') help='Do not remove data from cache')
+63 -50
View File
@@ -17,10 +17,10 @@ class ReproError(DvcException):
class CmdRepro(CmdRun): class CmdRepro(CmdRun):
def __init__(self): def __init__(self, args=None,parse_config=True, git_obj=None, config_obj=None):
CmdBase.__init__(self) CmdBase.__init__(self, args, parse_config, git_obj, config_obj)
self._code =[] self._code = []
pass pass
@property @property
@@ -30,11 +30,13 @@ class CmdRepro(CmdRun):
def define_args(self, parser): def define_args(self, parser):
self.set_skip_git_actions(parser) self.set_skip_git_actions(parser)
# self.add_string_arg(parser, 'target', 'Reproduce data file')
parser.add_argument('target', metavar='', help='Data file to reproduce', nargs='*') parser.add_argument('target', metavar='', help='Data file to reproduce', nargs='*')
pass pass
# Overridden methods:
@property
def is_reproducible(self):
return True
@property @property
def declaration_input_data_items(self): def declaration_input_data_items(self):
@@ -61,37 +63,41 @@ class CmdRepro(CmdRun):
format(self.config.data_dir, ' '.join(external_files_names))) format(self.config.data_dir, ' '.join(external_files_names)))
return 1 return 1
error = False if self.repro_data_items(data_item_list):
changed = False return self.commit_if_needed('DVC repro: {}'.format(' '.join(self.args.target)))
for data_item in data_item_list:
try:
repro_change = ReproChange(data_item, self)
if repro_change.reproduce():
changed = True
Logger.info(u'Data file "{}" was reproduced.'.format(
data_item.data.relative
))
else:
Logger.info(u'Reproduction is not required for data file "{}".'.format(
data_item.data.relative
))
except ReproError as err:
Logger.error('Error in reproducing data file {}: {}'.format(
data_item.data.relative, str(err)
))
error = True
break
if error and not self.skip_git_actions:
Logger.error('Errors occurred. One or more repro cmd was not successful.')
self.not_committed_changes_warning()
return 1
if changed:
self.commit_if_needed('DVC repro: {}'.format(' '.join(self.args.target)))
finally: finally:
lock.release() lock.release()
return 0
return 1
def repro_data_items(self, data_item_list):
error = False
changed = False
for data_item in data_item_list:
try:
repro_change = ReproChange(data_item, self)
if repro_change.reproduce():
changed = True
Logger.info(u'Data file "{}" was reproduced.'.format(
data_item.data.relative
))
else:
Logger.info(u'Reproduction is not required for data file "{}".'.format(
data_item.data.relative
))
except ReproError as err:
Logger.error('Error in reproducing data file {}: {}'.format(
data_item.data.relative, str(err)
))
error = True
break
if error and not self.skip_git_actions:
Logger.error('Errors occurred. One or more repro cmd was not successful.')
self.not_committed_changes_warning()
return changed and not error
class ReproChange(object): class ReproChange(object):
@@ -101,7 +107,7 @@ class ReproChange(object):
self._cmd_obj = cmd_obj self._cmd_obj = cmd_obj
self._state = StateFile.load(data_item.state.relative, self.git) self._state = StateFile.load(data_item.state.relative, self.git)
cmd_obj._code = self._state.code_dependencies # HACK!!! cmd_obj._code = self._state.code_dependencies
argv = self._state.norm_argv argv = self._state.norm_argv
@@ -112,16 +118,9 @@ class ReproChange(object):
raise ReproError('Error: reproducible cmd in state file "{}" is too short'. raise ReproError('Error: reproducible cmd in state file "{}" is too short'.
format(self._state.file)) format(self._state.file))
# if argv[0][-3:] != '.py':
# raise ReproError('Error: reproducible cmd format error in state file "{}"'.
# format(self._state.file))
self._repro_argv = argv self._repro_argv = argv
pass pass
def were_direct_dependencies_changed(self):
return True
def reproduce_data_file(self): def reproduce_data_file(self):
Logger.debug('Reproducing data file "{}". Removing the file...'.format( Logger.debug('Reproducing data file "{}". Removing the file...'.format(
self._data_item.data.relative)) self._data_item.data.relative))
@@ -134,21 +133,16 @@ class ReproChange(object):
return self._cmd_obj.run_command(self._repro_argv, data_items_from_args) return self._cmd_obj.run_command(self._repro_argv, data_items_from_args)
def reproduce(self, force=False): def reproduce(self, force=False):
print('+++++++++++++++ reproduce', self._data_item.data.dvc)
were_input_files_changed = False were_input_files_changed = False
if not self._state.is_reproducible: if not self._state.is_reproducible:
Logger.debug('Data file "{}" is not reproducible'.format(self._data_item.data.relative)) Logger.debug('Data file "{}" is not reproducible'.format(self._data_item.data.relative))
return False return False
for input_file in self._state.input_files: print('+++++++++++++++ reproduce dependencies: '.format(self.get_dependencies()))
try:
data_item = self._cmd_obj.path_factory.data_item(input_file)
except NotInDataDirError:
raise ReproError(u'The dependency files "{}" is not a data file'.format(input_file))
except Exception as ex:
raise ReproError(u'The dependency files "{}" can not be reproduced: {}'.format(
input_file, ex))
for data_item in self.get_dependencies():
change = ReproChange(data_item, self._cmd_obj) change = ReproChange(data_item, self._cmd_obj)
if change.reproduce(force): if change.reproduce(force):
were_input_files_changed = True were_input_files_changed = True
@@ -163,5 +157,24 @@ class ReproChange(object):
return self.reproduce_data_file() return self.reproduce_data_file()
def get_dependencies(self):
print('+++++++++++++== INPUT FILES: {}'.format(self._state.input_files))
dependency_data_items = []
for input_file in self._state.input_files:
try:
data_item = self._cmd_obj.path_factory.data_item(input_file)
except NotInDataDirError:
raise ReproError(u'The dependency file "{}" is not a data item'.format(input_file))
except Exception as ex:
raise ReproError(u'Unable to reproduced the dependency file "{}": {}'.format(
input_file, ex))
dependency_data_items.append(data_item)
return dependency_data_items
if __name__ == '__main__': if __name__ == '__main__':
run(CmdRepro()) run(CmdRepro())
+13 -4
View File
@@ -20,14 +20,17 @@ class RunError(DvcException):
class CmdRun(CmdBase): class CmdRun(CmdBase):
def __init__(self, parse_config=True, git_obj=None, config_obj=None): def __init__(self, args=None, parse_config=True, git_obj=None, config_obj=None):
super(CmdRun, self).__init__(parse_config, git_obj, config_obj) super(CmdRun, self).__init__(args, parse_config, git_obj, config_obj)
pass pass
def define_args(self, parser): def define_args(self, parser):
self.set_skip_git_actions(parser) self.set_skip_git_actions(parser)
parser.add_argument('--random', help='not reproducible, output is random', action='store_true') parser.add_argument('--not-repro',
help='Not reproducible',
action='store_false',
default=False)
parser.add_argument('--stdout', help='output std output to a file') parser.add_argument('--stdout', help='output std output to a file')
parser.add_argument('--stderr', help='output std error to a file') parser.add_argument('--stderr', help='output std error to a file')
parser.add_argument('--input', '-i', action='append', parser.add_argument('--input', '-i', action='append',
@@ -38,6 +41,10 @@ class CmdRun(CmdBase):
help='Code dependencies which produce the output') help='Code dependencies which produce the output')
pass pass
@property
def is_reproducible(self):
return not self.args.not_repro
@property @property
def code_dependencies(self): def code_dependencies(self):
return self.args.code or [] return self.args.code or []
@@ -93,11 +100,13 @@ class CmdRun(CmdBase):
data_item.move_data_to_cache() data_item.move_data_to_cache()
Logger.debug('Create state file "{}"'.format(data_item.state.relative)) Logger.debug('Create state file "{}"'.format(data_item.state.relative))
state_file = StateFile(data_item.state.relative, self.git, state_file = StateFile(data_item.state.relative, self.git,
input_files_dvc, input_files_dvc,
output_files_dvc, output_files_dvc,
code_dependencies_dvc, code_dependencies_dvc,
argv=argv) argv=argv,
is_reproducible=self.is_reproducible)
state_file.save() state_file.save()
result.append(state_file) result.append(state_file)
+10 -10
View File
@@ -10,7 +10,7 @@ class TestCmdDataRemove(DirHierarchyEnvironment):
DirHierarchyEnvironment.init_environment(self) DirHierarchyEnvironment.init_environment(self)
def test_file_by_file_removal(self): def test_file_by_file_removal(self):
cmd = CmdDataRemove(False, git_obj=self._git, config_obj=self._config) cmd = CmdDataRemove(parse_config=False, git_obj=self._git, config_obj=self._config)
cmd.args.keep_in_cloud = True cmd.args.keep_in_cloud = True
dir1_dvc_name = os.path.join('data', self.dir1) dir1_dvc_name = os.path.join('data', self.dir1)
@@ -20,7 +20,7 @@ class TestCmdDataRemove(DirHierarchyEnvironment):
self.assertFalse(os.path.exists(dir1_dvc_name)) self.assertFalse(os.path.exists(dir1_dvc_name))
def test_remove_deep_dir(self): def test_remove_deep_dir(self):
cmd = CmdDataRemove(False, git_obj=self._git, config_obj=self._config) cmd = CmdDataRemove(parse_config=False, git_obj=self._git, config_obj=self._config)
cmd.args.keep_in_cloud = True cmd.args.keep_in_cloud = True
cmd.args.recursive = True cmd.args.recursive = True
@@ -31,7 +31,7 @@ class TestCmdDataRemove(DirHierarchyEnvironment):
self.assertFalse(os.path.exists(dir1_dvc_name)) self.assertFalse(os.path.exists(dir1_dvc_name))
def test_not_recursive_removal(self): def test_not_recursive_removal(self):
cmd = CmdDataRemove(False, git_obj=self._git, config_obj=self._config) cmd = CmdDataRemove(parse_config=False, git_obj=self._git, config_obj=self._config)
cmd.args.keep_in_cloud = True cmd.args.keep_in_cloud = True
cmd.args.recursive = False cmd.args.recursive = False
@@ -43,7 +43,7 @@ class TestCmdDataRemove(DirHierarchyEnvironment):
pass pass
def test_data_dir_removal(self): def test_data_dir_removal(self):
cmd = CmdDataRemove(False, git_obj=self._git, config_obj=self._config) cmd = CmdDataRemove(parse_config=False, git_obj=self._git, config_obj=self._config)
cmd.args.keep_in_cloud = True cmd.args.keep_in_cloud = True
cmd.args.recursive = True cmd.args.recursive = True
@@ -60,7 +60,7 @@ class TestRemoveDataItem(DirHierarchyEnvironment):
DirHierarchyEnvironment.init_environment(self) DirHierarchyEnvironment.init_environment(self)
def test_remove_data_instance(self): def test_remove_data_instance(self):
cmd = CmdDataRemove(False, git_obj=self._git, config_obj=self._config) cmd = CmdDataRemove(parse_config=False, git_obj=self._git, config_obj=self._config)
cmd.args.keep_in_cloud = True cmd.args.keep_in_cloud = True
self.assertTrue(os.path.isfile(self.file1)) self.assertTrue(os.path.isfile(self.file1))
@@ -73,7 +73,7 @@ class TestRemoveDataItem(DirHierarchyEnvironment):
self.assertFalse(os.path.exists(self.state1)) self.assertFalse(os.path.exists(self.state1))
def test_keep_cache(self): def test_keep_cache(self):
cmd = CmdDataRemove(False, git_obj=self._git, config_obj=self._config) cmd = CmdDataRemove(parse_config=False, git_obj=self._git, config_obj=self._config)
cmd.args.keep_in_cloud = True cmd.args.keep_in_cloud = True
cmd.args.keep_in_cache = True cmd.args.keep_in_cache = True
@@ -87,7 +87,7 @@ class TestRemoveDataItem(DirHierarchyEnvironment):
self.assertFalse(os.path.exists(self.state1)) self.assertFalse(os.path.exists(self.state1))
def test_remove_data_instance_without_cache(self): def test_remove_data_instance_without_cache(self):
cmd = CmdDataRemove(False, git_obj=self._git, config_obj=self._config) cmd = CmdDataRemove(parse_config=False, git_obj=self._git, config_obj=self._config)
cmd.args.keep_in_cloud = True cmd.args.keep_in_cloud = True
self.assertTrue(os.path.isfile(self.file6)) self.assertTrue(os.path.isfile(self.file6))
@@ -103,7 +103,7 @@ class TestRemoveEndToEnd(DirHierarchyEnvironment):
DirHierarchyEnvironment.init_environment(self) DirHierarchyEnvironment.init_environment(self)
def test_end_to_end_with_an_error(self): def test_end_to_end_with_an_error(self):
cmd = CmdDataRemove(False, git_obj=self._git, config_obj=self._config) cmd = CmdDataRemove(parse_config=False, git_obj=self._git, config_obj=self._config)
cmd.args.keep_in_cloud = True cmd.args.keep_in_cloud = True
dir11_full = os.path.join('data', self.dir11) dir11_full = os.path.join('data', self.dir11)
@@ -130,7 +130,7 @@ class TestRemoveEndToEnd(DirHierarchyEnvironment):
self.assertTrue(os.path.exists(self.file6)) self.assertTrue(os.path.exists(self.file6))
def test_end_to_end_with_no_error(self): def test_end_to_end_with_no_error(self):
cmd = CmdDataRemove(False, git_obj=self._git, config_obj=self._config) cmd = CmdDataRemove(parse_config=False, git_obj=self._git, config_obj=self._config)
cmd.args.keep_in_cloud = True cmd.args.keep_in_cloud = True
dir11_full = os.path.join('data', self.dir11) dir11_full = os.path.join('data', self.dir11)
@@ -154,7 +154,7 @@ class TestRemoveEndToEnd(DirHierarchyEnvironment):
self.assertTrue(os.path.exists(self.file1)) self.assertTrue(os.path.exists(self.file1))
def test_run(self): def test_run(self):
cmd = CmdDataRemove(False, git_obj=self._git, config_obj=self._config) cmd = CmdDataRemove(parse_config=False, git_obj=self._git, config_obj=self._config)
cmd.args.keep_in_cloud = True cmd.args.keep_in_cloud = True
dir11_full = os.path.join('data', self.dir11) dir11_full = os.path.join('data', self.dir11)
+17 -14
View File
@@ -6,10 +6,10 @@ from unittest import TestCase
from dvc.command.run import CmdRun, RunError from dvc.command.run import CmdRun, RunError
from dvc.config import ConfigI from dvc.config import ConfigI
from dvc.executor import Executor
from dvc.path.data_item import DataItem from dvc.path.data_item import DataItem
from dvc.path.factory import PathFactory from dvc.path.factory import PathFactory
from dvc.git_wrapper import GitWrapperI from dvc.git_wrapper import GitWrapperI, GitWrapper
from dvc.repository_change import RepositoryChange
class RunBasicTest(TestCase): class RunBasicTest(TestCase):
@@ -24,32 +24,35 @@ class RunBasicTest(TestCase):
os.mkdir('cache') os.mkdir('cache')
os.mkdir('state') os.mkdir('state')
self._devnull = open(os.devnull, 'w') self.init_git_repo()
subprocess.Popen(['git', 'init'], stdout=self._devnull, stderr=None).wait() self.git = GitWrapper()
self.commit = 'abc1234'
self.git = GitWrapperI(self.test_dir, self.commit)
self.config = ConfigI('data', 'cache', 'state') self.config = ConfigI('data', 'cache', 'state')
self.path_factory = PathFactory(self.git, self.config) self.path_factory = PathFactory(self.git, self.config)
pass pass
def init_git_repo(self):
Executor.exec_cmd_only_success(['git', 'init'])
self.create_file('.gitignore', 'cache\n.dvc.conf.lock')
Executor.exec_cmd_only_success(['git', 'add', '.gitignore'])
Executor.exec_cmd_only_success(['git', 'commit', '-m', '"Init test repo"'])
@staticmethod
def create_file(file2, content='random text'):
fd = open(file2, 'w+')
fd.write(content)
fd.close()
def tearDown(self): def tearDown(self):
shutil.rmtree(self.test_dir, ignore_errors=True) shutil.rmtree(self.test_dir, ignore_errors=True)
os.chdir(self._old_curr_dir_abs) os.chdir(self._old_curr_dir_abs)
@staticmethod
def create_file(file2):
fd = open(file2, 'w+')
fd.write('random text')
fd.close()
class TestRunOutsideData(RunBasicTest): class TestRunOutsideData(RunBasicTest):
def test(self): def test(self):
cmd_run = CmdRun(config_obj=self.config) cmd_run = CmdRun(config_obj=self.config)
with self.assertRaises(RunError): with self.assertRaises(RunError):
state = cmd_run.run_command(['touch', 'file1', 'file2'], []) cmd_run.run_command(['touch', 'file1', 'file2'], [])
next(state)
pass pass
+134
View File
@@ -0,0 +1,134 @@
import os
import shutil
import subprocess
import tempfile
from unittest import TestCase
import sys
from dvc.command.remove import CmdDataRemove
from dvc.command.repro import ReproChange, CmdRepro
from dvc.command.run import CmdRun, RunError
from dvc.config import ConfigI
from dvc.executor import Executor
from dvc.path.data_item import DataItem
from dvc.path.factory import PathFactory
from dvc.git_wrapper import GitWrapperI, GitWrapper
from dvc.repository_change import RepositoryChange
from dvc.state_file import StateFile
from tests.test_cmd_run import RunBasicTest
# class ReproBasicEnv(TestCase):
# def setUp(self):
# self.test_dir = tempfile.mkdtemp()
# self._old_curr_dir_abs = os.path.realpath(os.curdir)
#
# self.tearDown()
# os.mkdir(self.test_dir)
# os.chdir(self.test_dir)
# os.mkdir('data')
# os.mkdir('cache')
# os.mkdir('state')
#
# self.init_git_repo()
# self.git = GitWrapper()
#
# self.config = ConfigI('data', 'cache', 'state')
# self.path_factory = PathFactory(self.git, self.config)
# pass
#
# def init_git_repo(self):
# Executor.exec_cmd_only_success(['git', 'init'])
# self.create_file('.gitignore', 'cache\n.dvc.conf.lock')
# Executor.exec_cmd_only_success(['git', 'add', '.gitignore'])
# Executor.exec_cmd_only_success(['git', 'commit', '-m', '"Init test repo"'])
#
# def tearDown(self):
# shutil.rmtree(self.test_dir, ignore_errors=True)
# os.chdir(self._old_curr_dir_abs)
#
# @staticmethod
# def create_file(file2, content='random text'):
# fd = open(file2, 'w+')
# fd.write(content)
# fd.close()
# class RunEndToEndTest()
class ReproBasicEnv(RunBasicTest):
def setUp(self):
super(ReproBasicEnv, self).setUp()
self.file_name1 = 'data/file1'
CmdRun(args=['printf', 'Hello\nWorld!', '--not-repro', '--stdout', self.file_name1],
parse_config=False,
config_obj=self.config,
git_obj=self.git
).run()
self.file_name11 = 'data/file11'
CmdRun(args=['head', '-n', '1', self.file_name1, '--stdout', self.file_name11],
parse_config=False,
config_obj=self.config,
git_obj=self.git
).run()
self.file_name2 = 'data/file2'
CmdRun(args=['printf', 'Bobby', '--not-repro', '--stdout', self.file_name2],
parse_config=False,
config_obj=self.config,
git_obj=self.git
).run()
self.file_name_res = 'data/file_res'
CmdRun(args=['cat', self.file_name11, self.file_name2, '--stdout', self.file_name_res],
parse_config=False,
config_obj=self.config,
git_obj=self.git
).run()
self.assertEqual(open(self.file_name_res).read(), 'Hello\nBobby')
#
# class ReproRemovedFileTest(ReproBasicEnv):
# def test(self):
# # CmdDataRemove(args=[self.file_name_res, '--keep-in-cloud'],
# # config_obj=self.config,
# # git_obj=self.git
# # ).run()
#
# # CmdRun(args=['printf', 'Goodbye\nWorld!', '--stdout', self.file_name1],
# # config_obj=self.config,
# # git_obj=self.git
# # ).run()
#
# CmdRepro(args=[self.file_name_res],
# config_obj=self.config,
# git_obj=self.git
# ).run()
#
# self.assertEqual(open(self.file_name_res).read(), 'Hello\nBobby')
class ReproChangedDependency(ReproBasicEnv):
def test(self):
self.assertEqual(open(self.file_name_res).read(), 'Hello\nBobby')
CmdDataRemove(args=[self.file_name1, '--keep-in-cloud'],
config_obj=self.config,
git_obj=self.git
).run()
CmdRun(args=['printf', 'Goodbye\nWorld!', '--stdout', self.file_name1],
config_obj=self.config,
git_obj=self.git
).run()
CmdRepro(args=[self.file_name_res],
config_obj=self.config,
git_obj=self.git
).run()
# self.assertEqual(open(self.file_name_res).read(), 'Goodbye\nBobby')