1import fileinput 2import os 3import shutil 4import unittest 5import sys 6try: 7 from StringIO import StringIO 8except ImportError: 9 from io import StringIO 10from mock import patch, MagicMock, call 11from six import assertRaisesRegex 12from txclib.commands import _set_source_file, _set_translation, cmd_pull, \ 13 cmd_init, cmd_config, cmd_status, cmd_help, UnInitializedError 14from txclib.cmdline import main 15from txclib.parsers import MAPPING, MAPPINGREMOTE, MAPPINGBULK 16 17 18class TestCommands(unittest.TestCase): 19 def test_cmd_pull_return_exception_when_dir_not_initialized(self): 20 """Test when tx is not instantiated, that proper error is thrown""" 21 with self.assertRaises(UnInitializedError): 22 cmd_pull([], None) 23 24 def test_set_source_file_when_dir_not_initialized(self): 25 with self.assertRaises(UnInitializedError): 26 _set_source_file(path_to_tx=None, resource='dummy_resource.en', 27 lang='en', path_to_file='dummy') 28 29 def test_set_translation_when_dir_not_initialized(self): 30 with self.assertRaises(UnInitializedError): 31 _set_translation(path_to_tx=None, resource="dummy_resource.md", 32 lang='en', path_to_file='invalid') 33 34 35class TestStatusCommand(unittest.TestCase): 36 @patch('txclib.commands.project') 37 def test_status(self, mock_p): 38 """Test status command""" 39 mock_project = MagicMock() 40 mock_project.get_chosen_resources.return_value = ['foo.bar'] 41 mock_project.get_resource_files.return_value = { 42 "fr": "translations/foo.bar/fr.po", 43 "de": "translations/foo.bar/de.po" 44 } 45 mock_p.Project.return_value = mock_project 46 cmd_status([], None) 47 mock_project.get_chosen_resources.assert_called_once_with([]) 48 self.assertEqual(mock_project.get_resource_files.call_count, 1) 49 50 51class TestHelpCommand(unittest.TestCase): 52 def test_help(self): 53 out = StringIO() 54 sys.stdout = out 55 cmd_help([], None) 56 output = out.getvalue().strip() 57 self.assertTrue( 58 all( 59 c in output for c in 60 ['delete', 'help', 'init', 'pull', 'push', 'set', 'status'] 61 ) 62 ) 63 64 # call for specific command 65 with patch('txclib.commands.cmd_pull', spec=cmd_pull) as pull_mock: 66 cmd_help(['pull'], None) 67 pull_mock.assert_called_once_with(['--help'], None) 68 69 70class TestInitCommand(unittest.TestCase): 71 72 def setUp(self): 73 self.curr_dir = os.getcwd() 74 self.config_file = '.tx/config' 75 os.chdir('./tests/project_dir/') 76 77 def tearDown(self, *args, **kwargs): 78 shutil.rmtree('.tx', ignore_errors=False, onerror=None) 79 os.chdir(self.curr_dir) 80 super(TestInitCommand, self).tearDown(*args, **kwargs) 81 82 def test_init(self): 83 argv = [] 84 config_text = "[main]\nhost = https://www.transifex.com\n\n" 85 with patch('txclib.commands.project.Project'): 86 with patch('txclib.commands.cmd_config') as set_mock: 87 cmd_init(argv, '') 88 set_mock.assert_called_once_with([], os.getcwd()) 89 self.assertTrue(os.path.exists('./.tx')) 90 self.assertTrue(os.path.exists('./.tx/config')) 91 self.assertEqual(open(self.config_file).read(), config_text) 92 93 def test_init_skipsetup(self): 94 argv = ['--skipsetup'] 95 with patch('txclib.commands.project.Project'): 96 with patch('txclib.commands.cmd_config') as set_mock: 97 cmd_init(argv, '') 98 self.assertEqual(set_mock.call_count, 0) 99 self.assertTrue(os.path.exists('./.tx')) 100 self.assertTrue(os.path.exists('./.tx/config')) 101 102 @patch('txclib.commands.utils.confirm') 103 def test_init_save_N(self, confirm_mock): 104 os.mkdir('./.tx') 105 open('./.tx/config', 'a').close() 106 argv = [] 107 confirm_mock.return_value = False 108 with patch('txclib.commands.project.Project') as project_mock: 109 cmd_init(argv, '') 110 self.assertEqual(project_mock.call_count, 0) 111 self.assertTrue(os.path.exists('./.tx')) 112 self.assertEqual(confirm_mock.call_count, 1) 113 114 @patch('txclib.commands.utils.confirm') 115 def test_init_save_y(self, confirm_mock): 116 os.mkdir('./.tx') 117 open('./.tx/config', 'a').close() 118 argv = [] 119 confirm_mock.return_value = True 120 with patch('txclib.commands.project.Project'): 121 with patch('txclib.commands.cmd_config') as set_mock: 122 cmd_init(argv, '') 123 set_mock.assert_called() 124 self.assertTrue(os.path.exists('./.tx')) 125 self.assertEqual(confirm_mock.call_count, 1) 126 127 def test_init_force_save(self): 128 os.mkdir('./.tx') 129 argv = ['--force-save'] 130 with patch('txclib.commands.project.Project'): 131 with patch('txclib.commands.cmd_config') as set_mock: 132 cmd_init(argv, '') 133 set_mock.assert_called() 134 self.assertTrue(os.path.exists('./.tx')) 135 self.assertTrue(os.path.exists('./.tx/config')) 136 137 138class TestPullCommand(unittest.TestCase): 139 @patch('txclib.utils.get_current_branch') 140 @patch('txclib.commands.logger') 141 @patch('txclib.commands.project.Project') 142 def test_pull_with_branch_no_git_repo(self, project_mock, log_mock, bmock): 143 bmock.return_value = None 144 pr_instance = MagicMock() 145 project_mock.return_value = pr_instance 146 with self.assertRaises(SystemExit): 147 cmd_pull(['--branch'], '.') 148 log_mock.error.assert_called_once_with( 149 "You specified the --branch option but current " 150 "directory does not seem to belong in any git repo.") 151 self.assertEqual(pr_instance.pull.call_count, 0) 152 153 @patch('txclib.utils.get_current_branch') 154 @patch('txclib.commands.logger') 155 @patch('txclib.commands.project.Project') 156 def test_pull_branch_git_repo(self, project_mock, log_mock, bmock): 157 bmock.return_value = 'a-branch' 158 pr_instance = MagicMock() 159 project_mock.return_value = pr_instance 160 cmd_pull(['--branch'], '.') 161 self.assertEqual(pr_instance.pull.call_count, 1) 162 pull_call = call( 163 branch='a-branch', fetchall=False, fetchsource=False, 164 force=False, languages=[], minimum_perc=None, mode=None, 165 overwrite=True, pseudo=False, resources=[], skip=False, 166 xliff=False, parallel=False, no_interactive=False 167 ) 168 pr_instance.pull.assert_has_calls([pull_call]) 169 170 @patch('txclib.utils.get_current_branch') 171 @patch('txclib.commands.logger') 172 @patch('txclib.commands.project.Project') 173 def test_pull_with_branch_and_branchname_option( 174 self, project_mock, log_mock, bmock 175 ): 176 pr_instance = MagicMock() 177 project_mock.return_value = pr_instance 178 bmock.return_value = None 179 cmd_pull(['--branch', 'somebranch'], '.') 180 self.assertEqual(pr_instance.pull.call_count, 1) 181 pull_call = call( 182 branch='somebranch', fetchall=False, fetchsource=False, 183 force=False, languages=[], minimum_perc=None, mode=None, 184 overwrite=True, pseudo=False, resources=[], skip=False, 185 xliff=False, parallel=False, no_interactive=False 186 ) 187 pr_instance.pull.assert_has_calls([pull_call]) 188 189 @patch('txclib.commands.project') 190 def test_pull_with_no_interactive(self, project_mock): 191 pr_instance = MagicMock() 192 pr_instance.pull.return_value = True 193 project_mock.Project.return_value = pr_instance 194 cmd_pull(['--no-interactive'], '.') 195 pull_call = call( 196 fetchall=False, force=False, minimum_perc=None, 197 skip=False, no_interactive=True, resources=[], pseudo=False, 198 languages=[], fetchsource=False, mode=None, branch=None, 199 xliff=False, parallel=False, overwrite=True 200 ) 201 self.assertEqual(pr_instance.pull.call_count, 1) 202 pr_instance.pull.assert_has_calls([pull_call]) 203 204 205class TestConfigCommand(unittest.TestCase): 206 207 def setUp(self): 208 self.curr_dir = os.getcwd() 209 os.chdir('./tests/project_dir/') 210 os.mkdir('.tx') 211 self.path_to_tx = os.getcwd() 212 self.config_file = '.tx/config' 213 self.config_fd = open(self.config_file, "w") 214 self.config_fd.write("[main]\nhost = https://foo.var\n") 215 self.config_fd.close() 216 217 def tearDown(self, *args, **kwargs): 218 shutil.rmtree('.tx', ignore_errors=False, onerror=None) 219 os.chdir(self.curr_dir) 220 super(TestConfigCommand, self).tearDown(*args, **kwargs) 221 222 def test_bare_set_too_few_arguments(self): 223 with self.assertRaises(SystemExit): 224 args = ["-r", "project1.resource1"] 225 cmd_config(args, None) 226 227 def test_bare_set_source_no_file(self): 228 with self.assertRaises(SystemExit): 229 args = ["-r", "project1.resource1", '--is-source', '-l', 'en'] 230 cmd_config(args, None) 231 232 with self.assertRaises(Exception): 233 args = ['-r', 'project1.resource1', '--source', '-l', 'en', 234 'noexistent-file.txt'] 235 cmd_config(args, self.path_to_tx) 236 237 def test_bare_set_source_file(self): 238 expected = ("[main]\nhost = https://foo.var\n\n[project1.resource1]\n" 239 "source_file = test.txt\nsource_lang = en\n\n") 240 args = ["-r", "project1.resource1", '--source', '-l', 'en', 'test.txt'] 241 cmd_config(args, self.path_to_tx) 242 with open(self.config_file) as config: 243 self.assertEqual(config.read(), expected) 244 245 # set translation file for de 246 expected = ("[main]\nhost = https://foo.var\n\n[project1.resource1]\n" 247 "source_file = test.txt\nsource_lang = en\n" 248 "trans.de = translations/de.txt\n\n") 249 args = ["-r", "project1.resource1", '-l', 'de', 'translations/de.txt'] 250 cmd_config(args, self.path_to_tx) 251 with open(self.config_file) as config: 252 self.assertEqual(config.read(), expected) 253 254 def test_auto_locale_no_expression(self): 255 error_msg = "You need to specify an expression" 256 with assertRaisesRegex(self, Exception, error_msg): 257 args = [MAPPING, "-r", "project1.resource1", 258 '--source-language', 'en'] 259 cmd_config(args, self.path_to_tx) 260 261 def test_auto_locale(self): 262 expected = "[main]\nhost = https://foo.var\n" 263 args = [MAPPING, "-r", "project1.resource1", '--source-language', 264 'en', 'translations/<lang>/test.txt'] 265 cmd_config(args, self.path_to_tx) 266 with open(self.config_file) as config: 267 self.assertEqual(config.read(), expected) 268 269 def test_auto_locale_is_backwards_compatible(self): 270 expected = ("[main]\nhost = https://foo.var\n\n[project1.resource1]\n" 271 "file_filter = translations/<lang>/test.txt\n" 272 "source_file = translations/en/test.txt\n" 273 "source_lang = en\n\n") 274 275 args = ["--auto-local", "-r", "project1.resource1", 276 '--source-language', 'en', '--execute', 277 'translations/<lang>/test.txt'] 278 cmd_config(args, self.path_to_tx) 279 with open(self.config_file) as config: 280 self.assertEqual(config.read(), expected) 281 282 def test_auto_locale_execute(self): 283 expected = ("[main]\nhost = https://foo.var\n\n[project1.resource1]\n" 284 "file_filter = translations/<lang>/test.txt\n" 285 "source_file = translations/en/test.txt\n" 286 "source_lang = en\n\n") 287 288 args = [MAPPING, "-r", "project1.resource1", '--source-language', 289 'en', '--execute', 'translations/<lang>/test.txt'] 290 cmd_config(args, self.path_to_tx) 291 with open(self.config_file) as config: 292 self.assertEqual(config.read(), expected) 293 294 def test_auto_remote_invalid_url(self): 295 # no project_url 296 args = [MAPPINGREMOTE] 297 with self.assertRaises(SystemExit): 298 cmd_config(args, self.path_to_tx) 299 300 # invalid project_url 301 args = [MAPPINGREMOTE, "http://some.random.url/"] 302 with self.assertRaises(Exception): 303 cmd_config(args, self.path_to_tx) 304 305 @patch('txclib.utils.get_details') 306 @patch('txclib.project.Project._extension_for') 307 def test_auto_remote_project(self, extension_mock, get_details_mock): 308 # change the host to tx 309 open(self.config_file, "w").write( 310 '[main]\nhost = https://www.transifex.com\n' 311 ) 312 expected = ("[main]\nhost = https://www.transifex.com\n\n" 313 "[proj.resource_1]\n" 314 "file_filter = translations/proj.resource_1/<lang>.txt\n" 315 "source_lang = fr\ntype = TXT\n\n[proj.resource_2]\n" 316 "file_filter = translations/proj.resource_2/<lang>.txt\n" 317 "source_lang = fr\ntype = TXT\n\n") 318 extension_mock.return_value = ".txt" 319 get_details_mock.side_effect = [ 320 # project details 321 { 322 'resources': [ 323 {'slug': 'resource_1', 'name': 'resource 1'}, 324 {'slug': 'resource_2', 'name': 'resource 2'} 325 ] 326 }, 327 # resources details 328 { 329 'source_language_code': 'fr', 330 'i18n_type': 'TXT', 331 'slug': 'resource_1', 332 }, { 333 'source_language_code': 'fr', 334 'i18n_type': 'TXT', 335 'slug': 'resource_2', 336 } 337 ] 338 args = [MAPPINGREMOTE, "https://www.transifex.com/test-org/proj/"] 339 cmd_config(args, self.path_to_tx) 340 with open(self.config_file) as config: 341 self.assertEqual(config.read(), expected) 342 343 @patch('txclib.utils.get_details') 344 @patch('txclib.project.Project._extension_for') 345 def test_auto_remote_is_backwards_compatible(self, extension_mock, 346 get_details_mock): 347 # change the host to tx 348 open(self.config_file, "w").write( 349 '[main]\nhost = https://www.transifex.com\n' 350 ) 351 expected = ("[main]\nhost = https://www.transifex.com\n\n" 352 "[proj.resource_1]\n" 353 "file_filter = translations/proj.resource_1/<lang>.txt\n" 354 "source_lang = fr\ntype = TXT\n\n[proj.resource_2]\n" 355 "file_filter = translations/proj.resource_2/<lang>.txt\n" 356 "source_lang = fr\ntype = TXT\n\n") 357 extension_mock.return_value = ".txt" 358 get_details_mock.side_effect = [ 359 # project details 360 { 361 'resources': [ 362 {'slug': 'resource_1', 'name': 'resource 1'}, 363 {'slug': 'resource_2', 'name': 'resource 2'} 364 ] 365 }, 366 # resources details 367 { 368 'source_language_code': 'fr', 369 'i18n_type': 'TXT', 370 'slug': 'resource_1', 371 }, { 372 'source_language_code': 'fr', 373 'i18n_type': 'TXT', 374 'slug': 'resource_2', 375 } 376 ] 377 args = ["--auto-remote", "https://www.transifex.com/test-org/proj/"] 378 cmd_config(args, self.path_to_tx) 379 with open(self.config_file) as config: 380 self.assertEqual(config.read(), expected) 381 382 def test_bulk_missing_options(self): 383 with self.assertRaises(SystemExit): 384 args = [MAPPINGBULK] 385 cmd_config(args, self.path_to_tx) 386 387 with self.assertRaises(SystemExit): 388 args = [MAPPINGBULK, "-p", "test-project"] 389 cmd_config(args, self.path_to_tx) 390 391 with self.assertRaises(SystemExit): 392 args = [MAPPINGBULK, "-p", "test-project", "--source-language", 393 "en", "--t", "TXT", "--file-extension", ".txt"] 394 cmd_config(args, self.path_to_tx) 395 396 def test_bulk(self): 397 expected = ("[main]\nhost = https://foo.var\n\n" 398 "[test-project.translations_en_test]\n" 399 "file_filter = translations/<lang>/en/test.txt\n" 400 "source_file = translations/en/test.txt\n" 401 "source_lang = en\ntype = TXT\n\n") 402 args = [MAPPINGBULK, "-p", "test-project", "--source-file-dir", 403 "translations", "--source-language", "en", "-t", "TXT", 404 "--file-extension", ".txt", "--execute", "--expression", 405 "translations/<lang>/{filepath}/{filename}{extension}"] 406 cmd_config(args, self.path_to_tx) 407 with open(self.config_file) as config: 408 self.assertEqual(config.read(), expected) 409 410 def test_invalid_expression(self): 411 args = [MAPPINGBULK, "-p", "test-project", "--source-file-dir", 412 "translations", "--source-language", "en", "-t", "TXT", 413 "--file-extension", ".txt", "--execute", "--expression", 414 "expression/{filename}{extension}"] 415 with self.assertRaises(SystemExit): 416 cmd_config(args, self.path_to_tx) 417 418 def test_invalid_expression_status(self): 419 """ 420 Test how the client handles invalid expressions in file_filter 421 """ 422 valid_expression = "test_expressions/<lang>/test.txt" 423 invalid_expression = "test_expressions/{filename}{ext}" 424 args = [MAPPINGBULK, "-p", "test-project", "--source-file-dir", 425 "test_expressions/en", "--source-language", "en", "-t", "TXT", 426 "--file-extension", ".txt", "--execute", "--expression", 427 valid_expression] 428 # Configure the client using a valid expression 429 cmd_config(args, self.path_to_tx) 430 431 # A trick to capture the standard output 432 sys_stdout = sys.stdout 433 out = StringIO() 434 sys.stdout = out 435 # Check for the expected status using the valid expression 436 cmd_status([], None) 437 output = out.getvalue().strip() 438 sys.stdout = sys_stdout 439 self.assertTrue("test_expressions/en/test.txt" in output) 440 self.assertTrue("test_expressions/es/test.txt" in output) 441 442 # Change the configuration file "by hand" and replace the file_filter 443 # with an invalid expression 444 for line in fileinput.input(self.config_file, inplace=True): 445 print(line.replace(valid_expression, invalid_expression)) 446 447 sys_stdout = sys.stdout 448 out = StringIO() 449 sys.stdout = out 450 cmd_status([], None) 451 output = out.getvalue().strip() 452 sys.stdout = sys_stdout 453 # Check that the Spanish translation file in not tracked 454 self.assertTrue("test_expressions/en/test.txt" in output) 455 self.assertFalse("test_expressions/es/test.txt" in output) 456 457 458class TestMainCommand(unittest.TestCase): 459 def test_call_tx_with_no_command(self): 460 with self.assertRaises(SystemExit): 461 main(['tx']) 462 463 def test_call_tx_with_invalid_command(self): 464 with self.assertRaises(SystemExit): 465 main(['tx', 'random']) 466