#!/usr/bin/env vpython3 # coding=utf-8 # Copyright (c) 2012 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Unit tests for fetch.py.""" import contextlib import logging import optparse import os import subprocess import sys import tempfile import unittest import distutils if sys.version_info.major == 2: from StringIO import StringIO import mock else: from io import StringIO from unittest import mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fetch class SystemExitMock(Exception): pass class UsageMock(Exception): pass class TestUtilityFunctions(unittest.TestCase): """This test case is against utility functions""" @mock.patch('sys.stdout', StringIO()) @mock.patch('os.listdir', return_value=['foo.py', 'bar']) @mock.patch('sys.exit', side_effect=SystemExitMock) def test_usage_with_message(self, exit_mock, listdir): with self.assertRaises(SystemExitMock): fetch.usage('foo') exit_mock.assert_called_once_with(1) listdir.assert_called_once() stdout = sys.stdout.getvalue() self.assertTrue(stdout.startswith('Error: foo')) self._usage_static_message(stdout) @mock.patch('sys.stdout', StringIO()) @mock.patch('os.listdir', return_value=['foo.py', 'bar']) @mock.patch('sys.exit', side_effect=SystemExitMock) def test_usage_with_no_message(self, exit_mock, listdir): with self.assertRaises(SystemExitMock): fetch.usage() exit_mock.assert_called_once_with(0) listdir.assert_called_once() self._usage_static_message(sys.stdout.getvalue()) def _usage_static_message(self, stdout): valid_fetch_config_text = 'Valid fetch configs:' self.assertIn(valid_fetch_config_text, stdout) # split[0] contains static text, whereas split[1] contains list of configs split = stdout.split(valid_fetch_config_text) self.assertEqual(2, len(split)) # verify a few fetch_configs self.assertIn('foo', split[1]) self.assertNotIn('bar', split[1]) @mock.patch('fetch.usage', side_effect=UsageMock) def test_handle_args_invalid_usage(self, usage): with self.assertRaises(UsageMock): fetch.handle_args(['filename', '-h']) usage.assert_called_with() with self.assertRaises(UsageMock): fetch.handle_args(['filename']) usage.assert_called_with('Must specify a config.') with self.assertRaises(UsageMock): fetch.handle_args(['filename', '--foo']) usage.assert_called_with('Invalid option --foo.') bad_arguments = [ '--not-valid-param', '-not-valid-param=1', '--not-valid-param=1=2' ] for bad_argument in bad_arguments: with self.assertRaises(UsageMock): fetch.handle_args(['filename', 'foo', bad_argument]) usage.assert_called_with('Got bad arguments [\'%s\']' % (bad_argument)) @mock.patch('optparse.Values', return_value=None) def test_handle_args_valid_usage(self, values): response = fetch.handle_args(['filename', 'foo']) values.assert_called_with({ 'dry_run': False, 'nohooks': False, 'no_history': False, 'force': False }) self.assertEqual((None, 'foo', []), response) response = fetch.handle_args([ 'filename', '-n', '--dry-run', '--nohooks', '--no-history', '--force', 'foo', '--some-param=1', '--bar=2' ]) values.assert_called_with({ 'dry_run': True, 'nohooks': True, 'no_history': True, 'force': True }) self.assertEqual((None, 'foo', ['--some-param=1', '--bar=2']), response) @mock.patch('os.path.exists', return_value=False) @mock.patch('sys.stdout', StringIO()) @mock.patch('sys.exit', side_effect=SystemExitMock) def test_run_config_fetch_not_found(self, exit_mock, exists): with self.assertRaises(SystemExitMock): fetch.run_config_fetch('foo', []) exit_mock.assert_called_with(1) exists.assert_called_once() self.assertEqual(1, len(exists.call_args[0])) self.assertTrue(exists.call_args[0][0].endswith('foo.py')) stdout = sys.stdout.getvalue() self.assertEqual('Could not find a config for foo\n', stdout) def test_run_config_fetch_integration(self): config = fetch.run_config_fetch('depot_tools', []) url = 'https://chromium.googlesource.com/chromium/tools/depot_tools.git' spec = { 'type': 'gclient_git', 'gclient_git_spec': { 'solutions': [{ 'url': url, 'managed': False, 'name': 'depot_tools', 'deps_file': 'DEPS', }], } } self.assertEqual((spec, 'depot_tools'), config) def test_checkout_factory(self): with self.assertRaises(KeyError): fetch.CheckoutFactory('invalid', {}, {}, "root") gclient = fetch.CheckoutFactory('gclient', {}, {}, "root") self.assertTrue(isinstance(gclient, fetch.GclientCheckout)) class TestCheckout(unittest.TestCase): def setUp(self): mock.patch('sys.stdout', StringIO()).start() self.addCleanup(mock.patch.stopall) self.opts = optparse.Values({'dry_run': False}) self.checkout = fetch.Checkout(self.opts, {}, '') @contextlib.contextmanager def _temporary_file(self): """Creates a temporary file and removes it once it's out of scope""" name = tempfile.mktemp() try: with open(name, 'w+') as f: yield f finally: os.remove(name) def test_run_dry(self): self.opts.dry_run = True self.checkout.run(['foo-not-found']) self.assertEqual('Running: foo-not-found\n', sys.stdout.getvalue()) def test_run_non_existing_command(self): with self.assertRaises(OSError): self.checkout.run(['foo-not-found']) self.assertEqual('Running: foo-not-found\n', sys.stdout.getvalue()) def test_run_non_existing_command_return_stdout(self): with self.assertRaises(OSError): self.checkout.run(['foo-not-found'], return_stdout=True) self.assertEqual('Running: foo-not-found\n', sys.stdout.getvalue()) @mock.patch('sys.stderr', StringIO()) @mock.patch('sys.exit', side_effect=SystemExitMock) def test_run_wrong_param(self, exit_mock): # mocked version of sys.std* is not passed to subprocess, use temp files with self._temporary_file() as f: with self.assertRaises(subprocess.CalledProcessError): self.checkout.run([sys.executable, '-invalid-param'], return_stdout=True, stderr=f) f.seek(0) # Expect some message to stderr self.assertNotEqual('', f.read()) self.assertEqual('', sys.stderr.getvalue()) with self._temporary_file() as f: with self.assertRaises(SystemExitMock): self.checkout.run([sys.executable, '-invalid-param'], stderr=f) f.seek(0) # Expect some message to stderr self.assertNotEqual('', f.read()) self.assertIn('Subprocess failed with return code', sys.stdout.getvalue()) exit_mock.assert_called_once() def test_run_return_as_value(self): cmd = ['python', '-c', 'print("foo")'] response = self.checkout.run(cmd, return_stdout=True) # we expect no response other than information about command self.assertNotIn('foo', sys.stdout.getvalue().split('\n')) # this file should be included in response self.assertEqual('foo', response.strip()) def test_run_print_to_stdout(self): cmd = ['python', '-c', 'print("foo")'] # mocked version of sys.std* is not passed to subprocess, use temp files with self._temporary_file() as stdout: with self._temporary_file() as stderr: response = self.checkout.run(cmd, stdout=stdout, stderr=stderr) stdout.seek(0) stderr.seek(0) self.assertEqual('foo\n', stdout.read()) self.assertEqual('', stderr.read()) stdout = sys.stdout.getvalue() self.assertEqual('', response) class TestGClientCheckout(unittest.TestCase): def setUp(self): self.run = mock.patch('fetch.Checkout.run').start() self.opts = optparse.Values({'dry_run': False}) self.checkout = fetch.GclientCheckout(self.opts, {}, '/root') self.addCleanup(mock.patch.stopall) @mock.patch('distutils.spawn.find_executable', return_value=True) def test_run_gclient_executable_found(self, find_executable): self.checkout.run_gclient('foo', 'bar', baz='qux') find_executable.assert_called_once_with('gclient') self.run.assert_called_once_with(('gclient', 'foo', 'bar'), baz='qux') @mock.patch('distutils.spawn.find_executable', return_value=False) def test_run_gclient_executable_not_found(self, find_executable): self.checkout.run_gclient('foo', 'bar', baz='qux') find_executable.assert_called_once_with('gclient') args = self.run.call_args[0][0] kargs = self.run.call_args[1] self.assertEqual(4, len(args)) self.assertEqual(sys.executable, args[0]) self.assertTrue(args[1].endswith('gclient.py')) self.assertEqual(('foo', 'bar'), args[2:]) self.assertEqual({'baz': 'qux'}, kargs) class TestGclientGitCheckout(unittest.TestCase): def setUp(self): self.run_gclient = mock.patch('fetch.GclientCheckout.run_gclient').start() self.run_git = mock.patch('fetch.GitCheckout.run_git').start() self.opts = optparse.Values({ 'dry_run': False, 'nohooks': True, 'no_history': False }) specs = { 'solutions': [{ 'foo': 'bar', 'baz': 1 }, { 'foo': False }], 'with_branch_heads': True, } self.checkout = fetch.GclientGitCheckout(self.opts, specs, '/root') self.addCleanup(mock.patch.stopall) def test_init(self): self.checkout.init() self.assertEqual(2, self.run_gclient.call_count) self.assertEqual(3, self.run_git.call_count) # Verify only expected commands and ignore arguments to avoid copying # commands from fetch.py self.assertEqual(['config', 'sync'], [a[0][0] for a in self.run_gclient.call_args_list]) self.assertEqual(['submodule', 'config', 'config'], [a[0][0] for a in self.run_git.call_args_list]) # First call to gclient, format spec is expected to be called so "foo" is # expected to be present args = self.run_gclient.call_args_list[0][0] self.assertEqual('config', args[0]) self.assertIn('foo', args[2]) if __name__ == '__main__': logging.basicConfig( level=logging.DEBUG if '-v' in sys.argv else logging.ERROR) unittest.main()