github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/jenkins/bootstrap_test.py (about)

     1  #!/usr/bin/env python
     2  
     3  # Copyright 2016 The Kubernetes Authors.
     4  #
     5  # Licensed under the Apache License, Version 2.0 (the "License");
     6  # you may not use this file except in compliance with the License.
     7  # You may obtain a copy of the License at
     8  #
     9  #     http://www.apache.org/licenses/LICENSE-2.0
    10  #
    11  # Unless required by applicable law or agreed to in writing, software
    12  # distributed under the License is distributed on an "AS IS" BASIS,
    13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  # See the License for the specific language governing permissions and
    15  # limitations under the License.
    16  
    17  """Tests for bootstrap."""
    18  
    19  # pylint: disable=protected-access, attribute-defined-outside-init
    20  
    21  import argparse
    22  import json
    23  import os
    24  import select
    25  import signal
    26  import subprocess
    27  import tempfile
    28  import time
    29  import unittest
    30  
    31  import bootstrap
    32  
    33  
    34  BRANCH = 'random_branch'
    35  BUILD = 'random_build'
    36  FAIL = ['/bin/bash', '-c', 'exit 1']
    37  JOB = 'random_job'
    38  PASS = ['/bin/bash', '-c', 'exit 0']
    39  PULL = 12345
    40  REPO = 'github.com/random_org/random_repo'
    41  ROBOT = 'fake-service-account.json'
    42  ROOT = '/random/root'
    43  UPLOAD = 'fake-gs://fake-bucket'
    44  
    45  
    46  class Stub(object):
    47      """Replace thing.param with replacement until exiting with."""
    48      def __init__(self, thing, param, replacement):
    49          self.thing = thing
    50          self.param = param
    51          self.replacement = replacement
    52          self.old = getattr(thing, param)
    53          setattr(thing, param, self.replacement)
    54  
    55      def __enter__(self, *a, **kw):
    56          return self.replacement
    57  
    58      def __exit__(self, *a, **kw):
    59          setattr(self.thing, self.param, self.old)
    60  
    61  
    62  class FakeCall(object):
    63      def __init__(self):
    64          self.calls = []
    65  
    66      def __call__(self, *a, **kw):
    67          self.calls.append((a, kw))
    68  
    69  
    70  class FakeSubprocess(object):
    71      """Keep track of calls."""
    72      def __init__(self):
    73          self.calls = []
    74          self.file_data = []
    75          self.output = {}
    76  
    77      def __call__(self, cmd, *a, **kw):
    78          self.calls.append((cmd, a, kw))
    79          for arg in cmd:
    80              if arg.startswith('/') and os.path.exists(arg):
    81                  self.file_data.append(open(arg).read())
    82          if kw.get('output') and self.output.get(cmd[0]):
    83              return self.output[cmd[0]].pop(0)
    84  
    85  
    86  # pylint: disable=invalid-name
    87  def Pass(*_a, **_kw):
    88      """Do nothing."""
    89      pass
    90  
    91  
    92  def Truth(*_a, **_kw):
    93      """Always true."""
    94      return True
    95  
    96  
    97  def Bomb(*a, **kw):
    98      """Always raise."""
    99      raise AssertionError('Should not happen', a, kw)
   100  # pylint: enable=invalid-name
   101  
   102  
   103  class ReadAllTest(unittest.TestCase):
   104      endless = 0
   105      ended = time.time() - 50
   106      number = 0
   107      end = -1
   108  
   109      def fileno(self):
   110          return self.end
   111  
   112      def readline(self):
   113          line = 'line %d\n' % self.number
   114          self.number += 1
   115          return line
   116  
   117      def test_read_more(self):
   118          """Read lines until we clear the buffer, noting there may be more."""
   119          lines = []
   120          total = 10
   121          def more_lines(*_a, **_kw):
   122              if len(lines) < total:
   123                  return [self], [], []
   124              return [], [], []
   125          with Stub(select, 'select', more_lines):
   126              done = bootstrap.read_all(self.endless, self, lines.append)
   127  
   128          self.assertFalse(done)
   129          self.assertEquals(total, len(lines))
   130          expected = ['line %d' % d for d in range(total)]
   131          self.assertEquals(expected, lines)
   132  
   133      def test_read_expired(self):
   134          """Read nothing as we are expired, noting there may be more."""
   135          lines = []
   136          with Stub(select, 'select', lambda *a, **kw: ([], [], [])):
   137              done = bootstrap.read_all(self.ended, self, lines.append)
   138  
   139          self.assertFalse(done)
   140          self.assertFalse(lines)
   141  
   142      def test_read_end(self):
   143          """Note we reached the end of the stream."""
   144          lines = []
   145          with Stub(select, 'select', lambda *a, **kw: ([self], [], [])):
   146              with Stub(self, 'readline', lambda: ''):
   147                  done = bootstrap.read_all(self.endless, self, lines.append)
   148  
   149          self.assertTrue(done)
   150  
   151  
   152  class TerminateTest(unittest.TestCase):
   153      """Tests for termiante()."""
   154      pid = 1234
   155      pgid = 5555
   156      terminated = False
   157      killed = False
   158  
   159      def terminate(self):
   160          self.terminated = True
   161  
   162      def kill(self):
   163          self.killed = True
   164  
   165      def getpgid(self, pid):
   166          self.got = pid
   167          return self.pgid
   168  
   169      def killpg(self, pgig, sig):
   170          self.killed_pg = (pgig, sig)
   171  
   172      def test_terminate_later(self):
   173          """Do nothing if end is in the future."""
   174          timeout = bootstrap.terminate(time.time() + 50, self, False)
   175          self.assertFalse(timeout)
   176  
   177      def test_terminate_never(self):
   178          """Do nothing if end is zero."""
   179          timeout = bootstrap.terminate(0, self, False)
   180          self.assertFalse(timeout)
   181  
   182      def test_terminate_terminate(self):
   183          """Terminate pid if after end and kill is false."""
   184          timeout = bootstrap.terminate(time.time() - 50, self, False)
   185          self.assertTrue(timeout)
   186          self.assertFalse(self.killed)
   187          self.assertTrue(self.terminated)
   188  
   189      def test_terminate_kill(self):
   190          """Kill process group if after end and kill is true."""
   191          with Stub(os, 'getpgid', self.getpgid), Stub(os, 'killpg', self.killpg):
   192              timeout = bootstrap.terminate(time.time() - 50, self, True)
   193          self.assertTrue(timeout)
   194          self.assertFalse(self.terminated)
   195          self.assertTrue(self.killed)
   196          self.assertEquals(self.pid, self.got)
   197          self.assertEquals(self.killed_pg, (self.pgid, signal.SIGKILL))
   198  
   199  
   200  class SubprocessTest(unittest.TestCase):
   201      """Tests for call()."""
   202  
   203      def test_stdin(self):
   204          """Will write to subprocess.stdin."""
   205          with self.assertRaises(subprocess.CalledProcessError) as cpe:
   206              bootstrap._call(0, ['/bin/bash'], stdin='exit 92')
   207          self.assertEquals(92, cpe.exception.returncode)
   208  
   209      def test_check_true(self):
   210          """Raise on non-zero exit codes if check is set."""
   211          with self.assertRaises(subprocess.CalledProcessError):
   212              bootstrap._call(0, FAIL, check=True)
   213  
   214          bootstrap._call(0, PASS, check=True)
   215  
   216      def test_check_default(self):
   217          """Default to check=True."""
   218          with self.assertRaises(subprocess.CalledProcessError):
   219              bootstrap._call(0, FAIL)
   220  
   221          bootstrap._call(0, PASS)
   222  
   223      @staticmethod
   224      def test_check_false():
   225          """Never raise when check is not set."""
   226          bootstrap._call(0, FAIL, check=False)
   227          bootstrap._call(0, PASS, check=False)
   228  
   229      def test_output(self):
   230          """Output is returned when requested."""
   231          cmd = ['/bin/bash', '-c', 'echo hello world']
   232          self.assertEquals(
   233              'hello world\n', bootstrap._call(0, cmd, output=True))
   234  
   235      def test_zombie(self):
   236          with self.assertRaises(subprocess.CalledProcessError):
   237              # make a zombie
   238              bootstrap._call(0, ['/bin/bash', '-c', 'A=$BASHPID && ( kill -STOP $A ) & exit 1'])
   239  
   240  
   241  class PullRefsTest(unittest.TestCase):
   242      """Tests for pull_ref, branch_ref, ref_has_shas, and pull_numbers."""
   243  
   244      def test_multiple_no_shas(self):
   245          """Test master,1111,2222."""
   246          self.assertEqual(
   247              bootstrap.pull_ref('master,123,456'),
   248              ([
   249                  'master',
   250                  '+refs/pull/123/head:refs/pr/123',
   251                  '+refs/pull/456/head:refs/pr/456',
   252              ], [
   253                  'FETCH_HEAD',
   254                  'refs/pr/123',
   255                  'refs/pr/456',
   256              ]),
   257          )
   258  
   259      def test_pull_has_shas(self):
   260          self.assertTrue(bootstrap.ref_has_shas('master:abcd'))
   261          self.assertFalse(bootstrap.ref_has_shas('123'))
   262          self.assertFalse(bootstrap.ref_has_shas(123))
   263          self.assertFalse(bootstrap.ref_has_shas(None))
   264  
   265      def test_pull_numbers(self):
   266          self.assertListEqual(bootstrap.pull_numbers(123), ['123'])
   267          self.assertListEqual(bootstrap.pull_numbers('master:abcd'), [])
   268          self.assertListEqual(
   269              bootstrap.pull_numbers('master:abcd,123:qwer,124:zxcv'),
   270              ['123', '124'])
   271  
   272      def test_pull_ref(self):
   273          self.assertEqual(
   274              bootstrap.pull_ref('master:abcd,123:effe'),
   275              (['master', '+refs/pull/123/head:refs/pr/123'], ['abcd', 'effe'])
   276          )
   277          self.assertEqual(
   278              bootstrap.pull_ref('123'),
   279              (['+refs/pull/123/merge'], ['FETCH_HEAD'])
   280          )
   281  
   282      def test_branch_ref(self):
   283          self.assertEqual(
   284              bootstrap.branch_ref('branch:abcd'),
   285              (['branch'], ['abcd'])
   286          )
   287          self.assertEqual(
   288              bootstrap.branch_ref('master'),
   289              (['master'], ['FETCH_HEAD'])
   290          )
   291  
   292  
   293  class ConfigureSshKeyTest(unittest.TestCase):
   294      """Tests for configure_ssh_key()."""
   295      def test_empty(self):
   296          """Do not change environ if no ssh key."""
   297          fake_env = {}
   298          with Stub(os, 'environ', fake_env):
   299              with bootstrap.configure_ssh_key(''):
   300                  self.assertFalse(fake_env)
   301  
   302      def test_full(self):
   303          fake_env = {}
   304          with Stub(os, 'environ', fake_env):
   305              with bootstrap.configure_ssh_key('hello there'):
   306                  self.assertIn('GIT_SSH', fake_env)
   307                  with open(fake_env['GIT_SSH']) as fp:
   308                      buf = fp.read()
   309                  self.assertIn('hello there', buf)
   310                  self.assertIn('ssh ', buf)
   311                  self.assertIn(' -i ', buf)
   312              self.assertFalse(fake_env)  # Resets env
   313  
   314      def test_full_old_value(self):
   315          fake_env = {'GIT_SSH': 'random-value'}
   316          old_env = dict(fake_env)
   317          with Stub(os, 'environ', fake_env):
   318              with bootstrap.configure_ssh_key('hello there'):
   319                  self.assertNotEqual(old_env, fake_env)
   320              self.assertEquals(old_env, fake_env)
   321  
   322  
   323  class CheckoutTest(unittest.TestCase):
   324      """Tests for checkout()."""
   325  
   326      def test_clean(self):
   327          """checkout cleans and resets if asked to."""
   328          fake = FakeSubprocess()
   329          with Stub(os, 'chdir', Pass):
   330              bootstrap.checkout(fake, REPO, REPO, None, PULL, clean=True)
   331  
   332          self.assertTrue(any(
   333              'clean' in cmd for cmd, _, _ in fake.calls if 'git' in cmd))
   334          self.assertTrue(any(
   335              'reset' in cmd for cmd, _, _ in fake.calls if 'git' in cmd))
   336  
   337      def test_fetch_retries(self):
   338          self.tries = 0
   339          expected_attempts = 3
   340          def third_time_charm(cmd, *_a, **_kw):
   341              if 'fetch' not in cmd:  # init/checkout are unlikely to fail
   342                  return
   343              self.tries += 1
   344              if self.tries != expected_attempts:
   345                  raise subprocess.CalledProcessError(128, cmd, None)
   346          with Stub(os, 'chdir', Pass):
   347              with Stub(time, 'sleep', Pass):
   348                  bootstrap.checkout(third_time_charm, REPO, REPO, None, PULL)
   349          self.assertEquals(expected_attempts, self.tries)
   350  
   351      def test_pull_ref(self):
   352          """checkout fetches the right ref for a pull."""
   353          fake = FakeSubprocess()
   354          with Stub(os, 'chdir', Pass):
   355              bootstrap.checkout(fake, REPO, REPO, None, PULL)
   356  
   357          expected_ref = bootstrap.pull_ref(PULL)[0][0]
   358          self.assertTrue(any(
   359              expected_ref in cmd for cmd, _, _ in fake.calls if 'fetch' in cmd))
   360  
   361      def test_branch(self):
   362          """checkout fetches the right ref for a branch."""
   363          fake = FakeSubprocess()
   364          with Stub(os, 'chdir', Pass):
   365              bootstrap.checkout(fake, REPO, REPO, BRANCH, None)
   366  
   367          expected_ref = BRANCH
   368          self.assertTrue(any(
   369              expected_ref in cmd for cmd, _, _ in fake.calls if 'fetch' in cmd))
   370  
   371      def test_repo(self):
   372          """checkout initializes and fetches the right repo."""
   373          fake = FakeSubprocess()
   374          with Stub(os, 'chdir', Pass):
   375              bootstrap.checkout(fake, REPO, REPO, BRANCH, None)
   376  
   377          expected_uri = 'https://%s' % REPO
   378          self.assertTrue(any(
   379              expected_uri in cmd for cmd, _, _ in fake.calls if 'fetch' in cmd))
   380  
   381      def test_branch_xor_pull(self):
   382          """Either branch or pull specified, not both."""
   383          with Stub(os, 'chdir', Bomb):
   384              with self.assertRaises(ValueError):
   385                  bootstrap.checkout(Bomb, REPO, REPO, None, None)
   386              with self.assertRaises(ValueError):
   387                  bootstrap.checkout(Bomb, REPO, REPO, BRANCH, PULL)
   388  
   389      def test_happy(self):
   390          """checkout sanity check."""
   391          fake = FakeSubprocess()
   392          with Stub(os, 'chdir', Pass):
   393              bootstrap.checkout(fake, REPO, REPO, BRANCH, None)
   394  
   395          self.assertTrue(any(
   396              '--tags' in cmd for cmd, _, _ in fake.calls if 'fetch' in cmd))
   397          self.assertTrue(any(
   398              'FETCH_HEAD' in cmd for cmd, _, _ in fake.calls
   399              if 'checkout' in cmd))
   400  
   401      def test_repo_path(self):
   402          """checkout repo to different local path."""
   403          fake = FakeSubprocess()
   404          repo_path = "foo/bar"
   405          with Stub(os, 'chdir', Pass):
   406              bootstrap.checkout(fake, REPO, repo_path, BRANCH, None)
   407  
   408          expected_uri = 'https://%s' % REPO
   409          self.assertTrue(any(
   410              expected_uri in cmd for cmd, _, _ in fake.calls if 'fetch' in cmd))
   411  
   412          self.assertTrue(any(
   413              repo_path in cmd for cmd, _, _ in fake.calls if 'init' in cmd))
   414  
   415  class ParseReposTest(unittest.TestCase):
   416      def test_bare(self):
   417          """--bare works."""
   418          args = bootstrap.parse_args(['--job=foo', '--bare'])
   419          self.assertFalse(bootstrap.parse_repos(args))
   420  
   421      def test_pull_branch_none(self):
   422          """args.pull and args.branch should be None"""
   423          args = bootstrap.parse_args(['--job=foo', '--bare'])
   424          self.assertIsNone(args.pull)
   425          self.assertIsNone(args.branch)
   426  
   427      def test_plain(self):
   428          """"--repo=foo equals foo=master."""
   429          args = bootstrap.parse_args(['--job=foo', '--repo=foo'])
   430          self.assertEquals(
   431              {'foo': ('master', '')},
   432              bootstrap.parse_repos(args))
   433  
   434      def test_branch(self):
   435          """--repo=foo=branch."""
   436          args = bootstrap.parse_args(['--job=foo', '--repo=foo=this'])
   437          self.assertEquals(
   438              {'foo': ('this', '')},
   439              bootstrap.parse_repos(args))
   440  
   441      def test_branch_commit(self):
   442          """--repo=foo=branch:commit works."""
   443          args = bootstrap.parse_args(['--job=foo', '--repo=foo=this:abcd'])
   444          self.assertEquals(
   445              {'foo': ('this:abcd', '')},
   446              bootstrap.parse_repos(args))
   447  
   448      def test_parse_repos(self):
   449          """--repo=foo=111,222 works"""
   450          args = bootstrap.parse_args(['--job=foo', '--repo=foo=111,222'])
   451          self.assertEquals(
   452              {'foo': ('', '111,222')},
   453              bootstrap.parse_repos(args))
   454  
   455      def test_pull_branch(self):
   456          """--repo=foo=master,111,222 works"""
   457          args = bootstrap.parse_args(['--job=foo', '--repo=foo=master,111,222'])
   458          self.assertEquals(
   459              {'foo': ('', 'master,111,222')},
   460              bootstrap.parse_repos(args))
   461  
   462      def test_pull_release_branch(self):
   463          """--repo=foo=release-3.14,&a-fancy%_branch+:abcd,222 works"""
   464          args = bootstrap.parse_args(['--job=foo',
   465                                       '--repo=foo=release-3.14,&a-fancy%_branch+:abcd,222'])
   466          self.assertEquals(
   467              {'foo': ('', 'release-3.14,&a-fancy%_branch+:abcd,222')},
   468              bootstrap.parse_repos(args))
   469  
   470      def test_pull_branch_commit(self):
   471          """--repo=foo=master,111,222 works"""
   472          args = bootstrap.parse_args(['--job=foo',
   473                                       '--repo=foo=master:aaa,111:bbb,222:ccc'])
   474          self.assertEquals(
   475              {'foo': ('', 'master:aaa,111:bbb,222:ccc')},
   476              bootstrap.parse_repos(args))
   477  
   478      def test_multi_repo(self):
   479          """--repo=foo=master,111,222 bar works"""
   480          args = bootstrap.parse_args(['--job=foo',
   481                                       '--repo=foo=master:aaa,111:bbb,222:ccc',
   482                                       '--repo=bar'])
   483          self.assertEquals(
   484              {
   485                  'foo': ('', 'master:aaa,111:bbb,222:ccc'),
   486                  'bar': ('master', '')},
   487              bootstrap.parse_repos(args))
   488  
   489  
   490  class GSUtilTest(unittest.TestCase):
   491      """Tests for GSUtil."""
   492      def test_upload_json(self):
   493          fake = FakeSubprocess()
   494          gsutil = bootstrap.GSUtil(fake)
   495          gsutil.upload_json('fake_path', {'wee': 'fun'})
   496          self.assertTrue(any(
   497              'application/json' in a for a in fake.calls[0][0]))
   498          self.assertEqual(fake.file_data, ['{\n  "wee": "fun"\n}'])
   499  
   500      def test_upload_text_cached(self):
   501          fake = FakeSubprocess()
   502          gsutil = bootstrap.GSUtil(fake)
   503          gsutil.upload_text('fake_path', 'hello world', cached=True)
   504          self.assertFalse(any(
   505              'Cache-Control' in a and 'max-age' in a
   506              for a in fake.calls[0][0]))
   507          self.assertEqual(fake.file_data, ['hello world'])
   508  
   509      def test_upload_text_default(self):
   510          fake = FakeSubprocess()
   511          gsutil = bootstrap.GSUtil(fake)
   512          gsutil.upload_text('fake_path', 'hello world')
   513          self.assertFalse(any(
   514              'Cache-Control' in a and 'max-age' in a
   515              for a in fake.calls[0][0]))
   516          self.assertEqual(fake.file_data, ['hello world'])
   517  
   518      def test_upload_text_uncached(self):
   519          fake = FakeSubprocess()
   520          gsutil = bootstrap.GSUtil(fake)
   521          gsutil.upload_text('fake_path', 'hello world', cached=False)
   522          self.assertTrue(any(
   523              'Cache-Control' in a and 'max-age' in a
   524              for a in fake.calls[0][0]))
   525          self.assertEqual(fake.file_data, ['hello world'])
   526  
   527      def test_upload_text_metalink(self):
   528          fake = FakeSubprocess()
   529          gsutil = bootstrap.GSUtil(fake)
   530          gsutil.upload_text('txt', 'path', additional_headers=['foo: bar'])
   531          self.assertTrue(any('foo: bar' in a for a in fake.calls[0][0]))
   532          self.assertEqual(fake.file_data, ['path'])
   533  
   534  class FakeGSUtil(object):
   535      generation = 123
   536  
   537      def __init__(self):
   538          self.cats = []
   539          self.jsons = []
   540          self.stats = []
   541          self.texts = []
   542  
   543      def cat(self, *a, **kw):
   544          self.cats.append((a, kw))
   545          return 'this is not a list'
   546  
   547      def stat(self, *a, **kw):
   548          self.stats.append((a, kw))
   549          return 'Generation: %s' % self.generation
   550  
   551      def upload_text(self, *args, **kwargs):
   552          self.texts.append((args, kwargs))
   553  
   554      def upload_json(self, *args, **kwargs):
   555          self.jsons.append((args, kwargs))
   556  
   557  class GubernatorUriTest(unittest.TestCase):
   558      def create_path(self, uri):
   559          self.fake_path = FakePath()
   560          self.fake_path.build_log = uri
   561          return self.fake_path
   562  
   563      def test_non_gs(self):
   564          uri = 'hello/world'
   565          self.assertEquals('hello', bootstrap.gubernator_uri(self.create_path(uri)))
   566  
   567      def test_multiple_gs(self):
   568          uri = 'gs://hello/gs://there'
   569          self.assertEquals(
   570              bootstrap.GUBERNATOR + '/hello/gs:',
   571              bootstrap.gubernator_uri(self.create_path(uri)))
   572  
   573      def test_gs(self):
   574          uri = 'gs://blah/blah/blah.txt'
   575          self.assertEquals(
   576              bootstrap.GUBERNATOR + '/blah/blah',
   577              bootstrap.gubernator_uri(self.create_path(uri)))
   578  
   579  
   580  class UploadPodspecTest(unittest.TestCase):
   581      """ Tests for maybe_upload_podspec() """
   582  
   583      def test_missing_env(self):
   584          """ Missing env vars return without doing anything. """
   585          # pylint: disable=no-self-use
   586          bootstrap.maybe_upload_podspec(None, '', None, {}.get)
   587          bootstrap.maybe_upload_podspec(None, '', None, {bootstrap.K8S_ENV: 'foo'}.get)
   588          bootstrap.maybe_upload_podspec(None, '', None, {'HOSTNAME': 'blah'}.get)
   589  
   590      def test_upload(self):
   591          gsutil = FakeGSUtil()
   592          call = FakeSubprocess()
   593  
   594          output = 'type: gamma/example\n'
   595          call.output['kubectl'] = [output]
   596          artifacts = 'gs://bucket/logs/123/artifacts'
   597          bootstrap.maybe_upload_podspec(
   598              call, artifacts, gsutil,
   599              {bootstrap.K8S_ENV: 'exists', 'HOSTNAME': 'abcd'}.get)
   600  
   601          self.assertEqual(
   602              call.calls,
   603              [(['kubectl', 'get', '-oyaml', 'pods/abcd'], (), {'output': True})])
   604          self.assertEqual(
   605              gsutil.texts,
   606              [(('%s/prow_podspec.yaml' % artifacts, output), {})])
   607  
   608  
   609  class AppendResultTest(unittest.TestCase):
   610      """Tests for append_result()."""
   611  
   612      def test_new_job(self):
   613          """Stat fails when the job doesn't exist."""
   614          gsutil = FakeGSUtil()
   615          build = 123
   616          version = 'v.interesting'
   617          success = True
   618          def fake_stat(*_a, **_kw):
   619              raise subprocess.CalledProcessError(1, ['gsutil'], None)
   620          gsutil.stat = fake_stat
   621          bootstrap.append_result(gsutil, 'fake_path', build, version, success)
   622          cache = gsutil.jsons[0][0][1]
   623          self.assertEquals(1, len(cache))
   624  
   625      def test_collision_cat(self):
   626          """cat fails if the cache has been updated."""
   627          gsutil = FakeGSUtil()
   628          build = 42
   629          version = 'v1'
   630          success = False
   631          generations = ['555', '444']
   632          orig_stat = gsutil.stat
   633          def fake_stat(*a, **kw):
   634              gsutil.generation = generations.pop()
   635              return orig_stat(*a, **kw)
   636          def fake_cat(_, gen):
   637              if gen == '555':  # Which version is requested?
   638                  return '[{"hello": 111}]'
   639              raise subprocess.CalledProcessError(1, ['gsutil'], None)
   640          with Stub(bootstrap, 'random_sleep', Pass):
   641              with Stub(gsutil, 'stat', fake_stat):
   642                  with Stub(gsutil, 'cat', fake_cat):
   643                      bootstrap.append_result(
   644                          gsutil, 'fake_path', build, version, success)
   645          self.assertIn('generation', gsutil.jsons[-1][1], gsutil.jsons)
   646          self.assertEquals('555', gsutil.jsons[-1][1]['generation'], gsutil.jsons)
   647  
   648      def test_collision_upload(self):
   649          """Test when upload_json tries to update an old version."""
   650          gsutil = FakeGSUtil()
   651          build = 42
   652          version = 'v1'
   653          success = False
   654          generations = [555, 444]
   655          orig = gsutil.upload_json
   656          def fake_upload(path, cache, generation):
   657              if generation == '555':
   658                  return orig(path, cache, generation=generation)
   659              raise subprocess.CalledProcessError(128, ['gsutil'], None)
   660          orig_stat = gsutil.stat
   661          def fake_stat(*a, **kw):
   662              gsutil.generation = generations.pop()
   663              return orig_stat(*a, **kw)
   664          def fake_cat(*_a, **_kw):
   665              return '[{"hello": 111}]'
   666          gsutil.stat = fake_stat
   667          gsutil.upload_json = fake_upload
   668          gsutil.cat = fake_cat
   669          with Stub(bootstrap, 'random_sleep', Pass):
   670              bootstrap.append_result(
   671                  gsutil, 'fake_path', build, version, success)
   672          self.assertIn('generation', gsutil.jsons[-1][1], gsutil.jsons)
   673          self.assertEquals('555', gsutil.jsons[-1][1]['generation'], gsutil.jsons)
   674  
   675      def test_handle_junk(self):
   676          gsutil = FakeGSUtil()
   677          gsutil.cat = lambda *a, **kw: '!@!$!@$@!$'
   678          build = 123
   679          version = 'v.interesting'
   680          success = True
   681          bootstrap.append_result(gsutil, 'fake_path', build, version, success)
   682          cache = gsutil.jsons[0][0][1]
   683          self.assertEquals(1, len(cache))
   684          self.assertIn(build, cache[0].values())
   685          self.assertIn(version, cache[0].values())
   686  
   687      def test_passed_is_bool(self):
   688          build = 123
   689          version = 'v.interesting'
   690          def try_run(success):
   691              gsutil = FakeGSUtil()
   692              bootstrap.append_result(gsutil, 'fake_path', build, version, success)
   693              cache = gsutil.jsons[0][0][1]
   694              self.assertTrue(isinstance(cache[0]['passed'], bool))
   695  
   696          try_run(1)
   697          try_run(0)
   698          try_run(None)
   699          try_run('')
   700          try_run('hello')
   701          try_run('true')
   702  
   703      def test_truncate(self):
   704          old = json.dumps({n: True for n in range(100000)})
   705          gsutil = FakeGSUtil()
   706          build = 123
   707          version = 'v.interesting'
   708          success = True
   709          bootstrap.append_result(gsutil, 'fake_path', build, version, success)
   710          cache = gsutil.jsons[0][0][1]
   711          self.assertLess(len(cache), len(old))
   712  
   713  
   714  class FinishTest(unittest.TestCase):
   715      """Tests for finish()."""
   716      def setUp(self):
   717          self.stubs = [
   718              Stub(bootstrap.GSUtil, 'upload_artifacts', Pass),
   719              Stub(bootstrap, 'append_result', Pass),
   720              Stub(os.path, 'isfile', Pass),
   721              Stub(os.path, 'isdir', Pass),
   722          ]
   723  
   724      def tearDown(self):
   725          for stub in self.stubs:
   726              with stub:
   727                  pass
   728  
   729      def test_no_version(self):
   730          gsutil = FakeGSUtil()
   731          paths = FakePath()
   732          success = True
   733          artifacts = 'not-a-dir'
   734          no_version = ''
   735          version = 'should not have found it'
   736          repos = repo({REPO: ('master', '')})
   737          with Stub(bootstrap, 'metadata', lambda *a: {'random-meta': version}):
   738              bootstrap.finish(gsutil, paths, success, artifacts,
   739                               BUILD, no_version, repos, FakeCall())
   740          bootstrap.finish(gsutil, paths, success, artifacts, BUILD, no_version, repos, FakeCall())
   741          calls = gsutil.jsons[-1]
   742          # json data is second positional argument
   743          self.assertNotIn('job-version', calls[0][1])
   744          self.assertNotIn('version', calls[0][1])
   745          self.assertTrue(calls[0][1].get('metadata'))
   746  
   747      def test_metadata_version(self):
   748          """Test that we will extract version info from metadata."""
   749          self.check_metadata_version('job-version')
   750          self.check_metadata_version('version')
   751  
   752      def check_metadata_version(self, key):
   753          gsutil = FakeGSUtil()
   754          paths = FakePath()
   755          success = True
   756          artifacts = 'not-a-dir'
   757          no_version = ''
   758          version = 'found it'
   759          with Stub(bootstrap, 'metadata', lambda *a: {key: version}):
   760              bootstrap.finish(gsutil, paths, success, artifacts, BUILD, no_version, REPO, FakeCall())
   761          calls = gsutil.jsons[-1]
   762          # Meta is second positional argument
   763          self.assertEquals(version, calls[0][1].get('job-version'))
   764          self.assertEquals(version, calls[0][1].get('version'))
   765  
   766      def test_ignore_err_up_artifacts(self):
   767          paths = FakePath()
   768          gsutil = FakeGSUtil()
   769          local_artifacts = None
   770          build = 123
   771          version = 'v1.terrible'
   772          success = True
   773          calls = []
   774          with Stub(os.path, 'isdir', lambda _: True):
   775              with Stub(os, 'walk', lambda d: [(True, True, True)]):
   776                  def fake_upload(*a, **kw):
   777                      calls.append((a, kw))
   778                      raise subprocess.CalledProcessError(1, ['fakecmd'], None)
   779                  gsutil.upload_artifacts = fake_upload
   780                  repos = repo({REPO: ('master', '')})
   781                  bootstrap.finish(
   782                      gsutil, paths, success, local_artifacts,
   783                      build, version, repos, FakeCall())
   784                  self.assertTrue(calls)
   785  
   786  
   787      def test_ignore_error_uploadtext(self):
   788          paths = FakePath()
   789          gsutil = FakeGSUtil()
   790          local_artifacts = None
   791          build = 123
   792          version = 'v1.terrible'
   793          success = True
   794          calls = []
   795          with Stub(os.path, 'isdir', lambda _: True):
   796              with Stub(os, 'walk', lambda d: [(True, True, True)]):
   797                  def fake_upload(*a, **kw):
   798                      calls.append((a, kw))
   799                      raise subprocess.CalledProcessError(1, ['fakecmd'], None)
   800                  gsutil.upload_artifacts = Pass
   801                  gsutil.upload_text = fake_upload
   802                  repos = repo({REPO: ('master', '')})
   803                  bootstrap.finish(
   804                      gsutil, paths, success, local_artifacts,
   805                      build, version, repos, FakeCall())
   806                  self.assertTrue(calls)
   807                  self.assertGreater(calls, 1)
   808  
   809      def test_skip_upload_artifacts(self):
   810          """Do not upload artifacts dir if it doesn't exist."""
   811          paths = FakePath()
   812          gsutil = FakeGSUtil()
   813          local_artifacts = None
   814          build = 123
   815          version = 'v1.terrible'
   816          success = True
   817          calls = []
   818          with Stub(os.path, 'isdir', lambda _: False):
   819              with Stub(bootstrap.GSUtil, 'upload_artifacts', Bomb):
   820                  repos = repo({REPO: ('master', '')})
   821                  bootstrap.finish(
   822                      gsutil, paths, success, local_artifacts,
   823                      build, version, repos, FakeCall())
   824                  self.assertFalse(calls)
   825  
   826  
   827  class MetadataTest(unittest.TestCase):
   828  
   829      def test_always_set_metadata(self):
   830          repos = repo({REPO: ('master', '')})
   831          meta = bootstrap.metadata(repos, 'missing-artifacts-dir', FakeCall())
   832          self.assertIn('repo', meta)
   833          self.assertEquals(REPO, meta['repo'])
   834  
   835      def test_multi_repo(self):
   836          repos = repo({REPO: ('foo', ''), 'other-repo': ('', '123,456')})
   837          meta = bootstrap.metadata(repos, 'missing-artifacts-dir', FakeCall())
   838          self.assertIn('repo', meta)
   839          self.assertEquals(REPO, meta['repo'])
   840          self.assertIn(REPO, meta.get('repos'))
   841          self.assertEquals('foo', meta['repos'][REPO])
   842          self.assertIn('other-repo', meta.get('repos'))
   843          self.assertEquals('123,456', meta['repos']['other-repo'])
   844  
   845  
   846  SECONDS = 10
   847  
   848  
   849  def fake_environment(set_home=True, set_node=True, set_job=True,
   850                       set_jenkins_home=True, set_workspace=True,
   851                       set_artifacts=True, **kwargs):
   852      if set_home:
   853          kwargs.setdefault(bootstrap.HOME_ENV, '/fake/home-dir')
   854      if set_node:
   855          kwargs.setdefault(bootstrap.NODE_ENV, 'fake-node')
   856      if set_job:
   857          kwargs.setdefault(bootstrap.JOB_ENV, JOB)
   858      if set_jenkins_home:
   859          kwargs.setdefault(bootstrap.JENKINS_HOME_ENV, '/fake/home-dir')
   860      if set_workspace:
   861          kwargs.setdefault(bootstrap.WORKSPACE_ENV, '/fake/workspace')
   862      if set_artifacts:
   863          kwargs.setdefault(bootstrap.JOB_ARTIFACTS_ENV, '/fake/workspace/_artifacts')
   864      return kwargs
   865  
   866  
   867  class BuildNameTest(unittest.TestCase):
   868      """Tests for build_name()."""
   869  
   870      def test_auto(self):
   871          """Automatically select a build if not done by user."""
   872          with Stub(os, 'environ', fake_environment()) as fake:
   873              bootstrap.build_name(SECONDS)
   874              self.assertTrue(fake[bootstrap.BUILD_ENV])
   875  
   876      def test_manual(self):
   877          """Respect user-selected build."""
   878          with Stub(os, 'environ', fake_environment()) as fake:
   879              truth = 'erick is awesome'
   880              fake[bootstrap.BUILD_ENV] = truth
   881              self.assertEquals(truth, fake[bootstrap.BUILD_ENV])
   882  
   883      def test_unique(self):
   884          """New build every minute."""
   885          with Stub(os, 'environ', fake_environment()) as fake:
   886              bootstrap.build_name(SECONDS)
   887              first = fake[bootstrap.BUILD_ENV]
   888              del fake[bootstrap.BUILD_ENV]
   889              bootstrap.build_name(SECONDS + 60)
   890              self.assertNotEqual(first, fake[bootstrap.BUILD_ENV])
   891  
   892  
   893  class SetupCredentialsTest(unittest.TestCase):
   894      """Tests for setup_credentials()."""
   895  
   896      def setUp(self):
   897          keys = {
   898              bootstrap.GCE_KEY_ENV: 'fake-key',
   899              bootstrap.SERVICE_ACCOUNT_ENV: 'fake-service-account.json',
   900          }
   901          self.env = fake_environment(**keys)
   902  
   903      def test_norobot_noupload_noenv(self):
   904          """Can avoid setting up credentials."""
   905          del self.env[bootstrap.SERVICE_ACCOUNT_ENV]
   906          with Stub(os, 'environ', self.env):
   907              bootstrap.setup_credentials(Bomb, None, None)
   908  
   909      def test_upload_no_robot_raises(self):
   910          del self.env[bootstrap.SERVICE_ACCOUNT_ENV]
   911          with Stub(os, 'environ', self.env):
   912              with self.assertRaises(ValueError):
   913                  bootstrap.setup_credentials(Pass, None, 'gs://fake')
   914  
   915  
   916      def test_application_credentials(self):
   917          """Raise if GOOGLE_APPLICATION_CREDENTIALS does not exist."""
   918          del self.env[bootstrap.SERVICE_ACCOUNT_ENV]
   919          with Stub(os, 'environ', self.env) as fake:
   920              gac = 'FAKE_CREDS.json'
   921              fake['HOME'] = 'kansas'
   922              with Stub(os.path, 'isfile', lambda p: p != gac):
   923                  with self.assertRaises(IOError):
   924                      bootstrap.setup_credentials(Pass, gac, UPLOAD)
   925  
   926              with Stub(os.path, 'isfile', Truth):
   927                  call = lambda *a, **kw: 'robot'
   928                  bootstrap.setup_credentials(call, gac, UPLOAD)
   929              # setup_creds should set SERVICE_ACCOUNT_ENV
   930              self.assertEquals(gac, fake.get(bootstrap.SERVICE_ACCOUNT_ENV))
   931              # now that SERVICE_ACCOUNT_ENV is set, it should try to activate
   932              # this
   933              with Stub(os.path, 'isfile', lambda p: p != gac):
   934                  with self.assertRaises(IOError):
   935                      bootstrap.setup_credentials(Pass, None, UPLOAD)
   936  
   937  
   938  class SetupMagicEnvironmentTest(unittest.TestCase):
   939      def test_home_workspace_on_jenkins(self):
   940          """WORKSPACE/HOME are set correctly for the Jenkins environment."""
   941          env = fake_environment(set_jenkins_home=True, set_workspace=True)
   942          cwd = '/fake/random-location'
   943          old_home = env[bootstrap.HOME_ENV]
   944          old_workspace = env[bootstrap.WORKSPACE_ENV]
   945          with Stub(os, 'environ', env):
   946              with Stub(os, 'getcwd', lambda: cwd):
   947                  bootstrap.setup_magic_environment(JOB, FakeCall())
   948  
   949          self.assertIn(bootstrap.WORKSPACE_ENV, env)
   950          self.assertNotEquals(env[bootstrap.HOME_ENV],
   951                               env[bootstrap.WORKSPACE_ENV])
   952          self.assertNotEquals(old_home, env[bootstrap.HOME_ENV])
   953          self.assertEquals(cwd, env[bootstrap.HOME_ENV])
   954          self.assertEquals(old_workspace, env[bootstrap.WORKSPACE_ENV])
   955          self.assertNotEquals(cwd, env[bootstrap.WORKSPACE_ENV])
   956  
   957      def test_home_workspace_in_k8s(self):
   958          """WORKSPACE/HOME are set correctly for the kubernetes environment."""
   959          env = fake_environment(set_jenkins_home=False, set_workspace=True)
   960          cwd = '/fake/random-location'
   961          old_home = env[bootstrap.HOME_ENV]
   962          old_workspace = env[bootstrap.WORKSPACE_ENV]
   963          with Stub(os, 'environ', env):
   964              with Stub(os, 'getcwd', lambda: cwd):
   965                  bootstrap.setup_magic_environment(JOB, FakeCall())
   966  
   967          self.assertIn(bootstrap.WORKSPACE_ENV, env)
   968          self.assertNotEquals(env[bootstrap.HOME_ENV],
   969                               env[bootstrap.WORKSPACE_ENV])
   970          self.assertEquals(old_home, env[bootstrap.HOME_ENV])
   971          self.assertNotEquals(cwd, env[bootstrap.HOME_ENV])
   972          self.assertEquals(old_workspace, env[bootstrap.WORKSPACE_ENV])
   973          self.assertNotEquals(cwd, env[bootstrap.WORKSPACE_ENV])
   974  
   975      def test_workspace_always_set(self):
   976          """WORKSPACE is set to cwd when unset in initial environment."""
   977          env = fake_environment(set_workspace=False)
   978          cwd = '/fake/random-location'
   979          with Stub(os, 'environ', env):
   980              with Stub(os, 'getcwd', lambda: cwd):
   981                  bootstrap.setup_magic_environment(JOB, FakeCall())
   982  
   983          self.assertIn(bootstrap.WORKSPACE_ENV, env)
   984          self.assertEquals(cwd, env[bootstrap.HOME_ENV])
   985          self.assertEquals(cwd, env[bootstrap.WORKSPACE_ENV])
   986  
   987      def test_job_env_mismatch(self):
   988          env = fake_environment()
   989          with Stub(os, 'environ', env):
   990              self.assertNotEquals('this-is-a-job', env[bootstrap.JOB_ENV])
   991              bootstrap.setup_magic_environment('this-is-a-job', FakeCall())
   992              self.assertEquals('this-is-a-job', env[bootstrap.JOB_ENV])
   993  
   994      def test_expected(self):
   995          env = fake_environment()
   996          del env[bootstrap.JOB_ENV]
   997          del env[bootstrap.NODE_ENV]
   998          with Stub(os, 'environ', env):
   999              # call is only used to git show the HEAD commit, so give a fake
  1000              # timestamp in return
  1001              bootstrap.setup_magic_environment(JOB, lambda *a, **kw: '123456\n')
  1002  
  1003          def check(name):
  1004              self.assertIn(name, env)
  1005  
  1006          # Some of these are probably silly to check...
  1007          # TODO(fejta): remove as many of these from our infra as possible.
  1008          check(bootstrap.JOB_ENV)
  1009          check(bootstrap.CLOUDSDK_ENV)
  1010          check(bootstrap.BOOTSTRAP_ENV)
  1011          check(bootstrap.WORKSPACE_ENV)
  1012          self.assertNotIn(bootstrap.SERVICE_ACCOUNT_ENV, env)
  1013          self.assertEquals(env[bootstrap.SOURCE_DATE_EPOCH_ENV], '123456')
  1014  
  1015      def test_node_present(self):
  1016          expected = 'whatever'
  1017          env = {bootstrap.NODE_ENV: expected}
  1018          with Stub(os, 'environ', env):
  1019              self.assertEquals(expected, bootstrap.node())
  1020          self.assertEquals(expected, env[bootstrap.NODE_ENV])
  1021  
  1022      def test_node_missing(self):
  1023          env = {}
  1024          with Stub(os, 'environ', env):
  1025              expected = bootstrap.node()
  1026              self.assertTrue(expected)
  1027          self.assertEquals(expected, env[bootstrap.NODE_ENV])
  1028  
  1029  
  1030  
  1031      def test_cloud_sdk_config(self):
  1032          cwd = 'now-here'
  1033          env = fake_environment()
  1034          with Stub(os, 'environ', env):
  1035              with Stub(os, 'getcwd', lambda: cwd):
  1036                  bootstrap.setup_magic_environment(JOB, FakeCall())
  1037  
  1038  
  1039          self.assertTrue(env[bootstrap.CLOUDSDK_ENV].startswith(cwd))
  1040  
  1041  
  1042  class FakePath(object):
  1043      artifacts = 'fake_artifacts'
  1044      pr_latest = 'fake_pr_latest'
  1045      pr_build_link = 'fake_pr_link'
  1046      build_log = 'fake_log_path'
  1047      pr_path = 'fake_pr_path'
  1048      pr_result_cache = 'fake_pr_result_cache'
  1049      latest = 'fake_latest'
  1050      result_cache = 'fake_result_cache'
  1051      started = 'fake_started.json'
  1052      finished = 'fake_finished.json'
  1053      def __call__(self, *arg, **kw):
  1054          self.arg = arg
  1055          self.kw = kw
  1056          return self
  1057  
  1058  
  1059  class FakeLogging(object):
  1060      close = Pass
  1061      def __call__(self, *_a, **_kw):
  1062          return self
  1063  
  1064  
  1065  class FakeFinish(object):
  1066      called = False
  1067      result = None
  1068      def __call__(self, unused_a, unused_b, success, *a, **kw):
  1069          self.called = True
  1070          self.result = success
  1071  
  1072  def repo(config):
  1073      repos = bootstrap.Repos()
  1074      for cur_repo, tup in config.items():
  1075          repos[cur_repo] = tup
  1076      return repos
  1077  
  1078  class PRPathsTest(unittest.TestCase):
  1079      def test_kubernetes_kubernetes(self):
  1080          """Test the kubernetes/kubernetes prefix."""
  1081          path = bootstrap.pr_paths(UPLOAD, repo({'kubernetes/kubernetes': ('', PULL)}), JOB, BUILD)
  1082          self.assertTrue(any(
  1083              str(PULL) == p for p in path.build_log.split('/')))
  1084  
  1085      def test_kubernetes(self):
  1086          """Test the kubernetes/something prefix."""
  1087          path = bootstrap.pr_paths(UPLOAD, repo({'kubernetes/prefix': ('', PULL)}), JOB, BUILD)
  1088          self.assertTrue(any(
  1089              'prefix' in p for p in path.build_log.split('/')), path.build_log)
  1090          self.assertTrue(any(
  1091              str(PULL) in p for p in path.build_log.split('/')), path.build_log)
  1092  
  1093      def test_other(self):
  1094          """Test the none kubernetes prefixes."""
  1095          path = bootstrap.pr_paths(UPLOAD, repo({'github.com/random/repo': ('', PULL)}), JOB, BUILD)
  1096          self.assertTrue(any(
  1097              'random_repo' in p for p in path.build_log.split('/')), path.build_log)
  1098          self.assertTrue(any(
  1099              str(PULL) in p for p in path.build_log.split('/')), path.build_log)
  1100  
  1101  
  1102  class FakeArgs(object):
  1103      bare = False
  1104      clean = False
  1105      git_cache = ''
  1106      job = JOB
  1107      root = ROOT
  1108      service_account = ROBOT
  1109      ssh = False
  1110      timeout = 0
  1111      upload = UPLOAD
  1112      json = False
  1113      scenario = ''
  1114  
  1115      def __init__(self, **kw):
  1116          self.branch = BRANCH
  1117          self.pull = PULL
  1118          self.repo = [REPO]
  1119          self.extra_job_args = []
  1120          for key, val in kw.items():
  1121              if not hasattr(self, key):
  1122                  raise AttributeError(self, key)
  1123              setattr(self, key, val)
  1124  
  1125  
  1126  def test_bootstrap(**kw):
  1127      if isinstance(kw.get('repo'), basestring):
  1128          kw['repo'] = [kw['repo']]
  1129      return bootstrap.bootstrap(FakeArgs(**kw))
  1130  
  1131  class BootstrapTest(unittest.TestCase):
  1132  
  1133      def setUp(self):
  1134          self.boiler = [
  1135              Stub(bootstrap, 'checkout', Pass),
  1136              Stub(bootstrap, 'finish', Pass),
  1137              Stub(bootstrap.GSUtil, 'copy_file', Pass),
  1138              Stub(bootstrap, 'node', lambda: 'fake-node'),
  1139              Stub(bootstrap, 'setup_credentials', Pass),
  1140              Stub(bootstrap, 'setup_logging', FakeLogging()),
  1141              Stub(bootstrap, 'start', Pass),
  1142              Stub(bootstrap, '_call', Pass),
  1143              Stub(os, 'environ', fake_environment()),
  1144              Stub(os, 'chdir', Pass),
  1145              Stub(os, 'makedirs', Pass),
  1146          ]
  1147  
  1148      def tearDown(self):
  1149          for stub in self.boiler:
  1150              with stub:  # Leaving with restores things
  1151                  pass
  1152  
  1153      def test_setcreds_setroot_fails(self):
  1154          """We should still call setup_credentials even if setup_root blows up."""
  1155          called = set()
  1156          with Stub(bootstrap, 'setup_root', Bomb):
  1157              with Stub(bootstrap, 'setup_credentials',
  1158                        lambda *a, **kw: called.add('setup_credentials')):
  1159                  with Stub(bootstrap, 'finish', lambda *a, **kw: called.add('finish')):
  1160                      with self.assertRaises(AssertionError):
  1161                          test_bootstrap()
  1162  
  1163          for needed in ['setup_credentials', 'finish']:
  1164              self.assertIn(needed, called)
  1165  
  1166      def test_empty_repo(self):
  1167          repo_name = None
  1168          with Stub(bootstrap, 'checkout', Bomb):
  1169              test_bootstrap(repo=repo_name, branch=None, pull=None, bare=True)
  1170          with self.assertRaises(ValueError):
  1171              test_bootstrap(repo=repo_name, branch=None, pull=PULL)
  1172          with self.assertRaises(ValueError):
  1173              test_bootstrap(repo=repo_name, branch=BRANCH, pull=None)
  1174  
  1175      def test_root_not_exists(self):
  1176          with Stub(os, 'chdir', FakeCall()) as fake_chdir:
  1177              with Stub(os.path, 'exists', lambda p: False):
  1178                  with Stub(os, 'makedirs', FakeCall()) as fake_makedirs:
  1179                      test_bootstrap(branch=None)
  1180          self.assertTrue(any(ROOT in c[0] for c in fake_chdir.calls), fake_chdir.calls)
  1181          self.assertTrue(any(ROOT in c[0] for c in fake_makedirs.calls), fake_makedirs.calls)
  1182  
  1183      def test_root_exists(self):
  1184          with Stub(os, 'chdir', FakeCall()) as fake_chdir:
  1185              test_bootstrap(branch=None)
  1186          self.assertTrue(any(ROOT in c[0] for c in fake_chdir.calls))
  1187  
  1188      def test_pr_paths(self):
  1189          """Use a pr_paths when pull is set."""
  1190  
  1191          with Stub(bootstrap, 'ci_paths', Bomb):
  1192              with Stub(bootstrap, 'pr_paths', FakePath()) as path:
  1193                  test_bootstrap(branch=None, pull=PULL, root='.')
  1194              self.assertEquals(PULL, path.arg[1][REPO][1], (PULL, path.arg))
  1195  
  1196      def test_ci_paths(self):
  1197          """Use a ci_paths when branch is set."""
  1198  
  1199          with Stub(bootstrap, 'pr_paths', Bomb):
  1200              with Stub(bootstrap, 'ci_paths', FakePath()) as path:
  1201                  test_bootstrap(pull=None, branch=BRANCH)
  1202              self.assertFalse(any(
  1203                  PULL in o for o in (path.arg, path.kw)))
  1204  
  1205      def test_finish_when_start_fails(self):
  1206          """Finish is called even if start fails."""
  1207          with Stub(bootstrap, 'finish', FakeFinish()) as fake:
  1208              with Stub(bootstrap, 'start', Bomb):
  1209                  with self.assertRaises(AssertionError):
  1210                      test_bootstrap(pull=None)
  1211          self.assertTrue(fake.called)
  1212          self.assertTrue(fake.result is False)  # Distinguish from None
  1213  
  1214      def test_finish_when_build_fails(self):
  1215          """Finish is called even if the build fails."""
  1216          def call_error(*_a, **_kw):
  1217              raise subprocess.CalledProcessError(1, [], '')
  1218          with Stub(bootstrap, 'finish', FakeFinish()) as fake:
  1219              with Stub(bootstrap, '_call', call_error):
  1220                  with self.assertRaises(SystemExit):
  1221                      test_bootstrap(pull=None)
  1222          self.assertTrue(fake.called)
  1223          self.assertTrue(fake.result is False)  # Distinguish from None
  1224  
  1225      def test_happy(self):
  1226          with Stub(bootstrap, 'finish', FakeFinish()) as fake:
  1227              test_bootstrap(pull=None)
  1228          self.assertTrue(fake.called)
  1229          self.assertTrue(fake.result)  # Distinguish from None
  1230  
  1231      def test_job_env(self):
  1232          """bootstrap sets JOB_NAME."""
  1233          with Stub(os, 'environ', fake_environment()) as env:
  1234              test_bootstrap(pull=None)
  1235          self.assertIn(bootstrap.JOB_ENV, env)
  1236  
  1237      def test_job_script_expands_vars(self):
  1238          fake = {
  1239              'HELLO': 'awesome',
  1240              'WORLD': 'sauce',
  1241          }
  1242          with Stub(os, 'environ', fake):
  1243              actual = bootstrap.job_args(
  1244                  ['$HELLO ${WORLD}', 'happy', '${MISSING}'])
  1245          self.assertEquals(['awesome sauce', 'happy', '${MISSING}'], actual)
  1246  
  1247  
  1248  class RepositoryTest(unittest.TestCase):
  1249      def test_kubernetes_kubernetes(self):
  1250          expected = 'https://github.com/kubernetes/kubernetes'
  1251          actual = bootstrap.repository('k8s.io/kubernetes', '')
  1252          self.assertEquals(expected, actual)
  1253  
  1254      def test_kubernetes_testinfra(self):
  1255          expected = 'https://github.com/kubernetes/test-infra'
  1256          actual = bootstrap.repository('k8s.io/test-infra', '')
  1257          self.assertEquals(expected, actual)
  1258  
  1259      def test_whatever(self):
  1260          expected = 'https://foo.com/bar'
  1261          actual = bootstrap.repository('foo.com/bar', '')
  1262          self.assertEquals(expected, actual)
  1263  
  1264      def test_k8s_k8s_ssh(self):
  1265          expected = 'git@github.com:kubernetes/kubernetes'
  1266          actual = bootstrap.repository('k8s.io/kubernetes', 'path')
  1267          self.assertEquals(expected, actual)
  1268  
  1269      def test_k8s_k8s_ssh_with_colon(self):
  1270          expected = 'git@github.com:kubernetes/kubernetes'
  1271          actual = bootstrap.repository('github.com:kubernetes/kubernetes', 'path')
  1272          self.assertEquals(expected, actual)
  1273  
  1274      def test_whatever_ssh(self):
  1275          expected = 'git@foo.com:bar'
  1276          actual = bootstrap.repository('foo.com/bar', 'path')
  1277          self.assertEquals(expected, actual)
  1278  
  1279  
  1280  
  1281  class IntegrationTest(unittest.TestCase):
  1282      REPO = 'hello/world'
  1283      MASTER = 'fake-master-file'
  1284      BRANCH_FILE = 'fake-branch-file'
  1285      PR_FILE = 'fake-pr-file'
  1286      BRANCH = 'another-branch'
  1287      PR_NUM = 42
  1288      PR_TAG = bootstrap.pull_ref(PR_NUM)[0][0].strip('+')
  1289  
  1290      def fake_repo(self, fake, _ssh=False):
  1291          return os.path.join(self.root_github, fake)
  1292  
  1293      def setUp(self):
  1294          self.boiler = [
  1295              Stub(bootstrap, 'finish', Pass),
  1296              Stub(bootstrap.GSUtil, 'copy_file', Pass),
  1297              Stub(bootstrap, 'repository', self.fake_repo),
  1298              Stub(bootstrap, 'setup_credentials', Pass),
  1299              Stub(bootstrap, 'setup_logging', FakeLogging()),
  1300              Stub(bootstrap, 'start', Pass),
  1301              Stub(os, 'environ', fake_environment(set_job=False)),
  1302          ]
  1303          self.root_github = tempfile.mkdtemp()
  1304          self.root_workspace = tempfile.mkdtemp()
  1305          self.root_git_cache = tempfile.mkdtemp()
  1306          self.ocwd = os.getcwd()
  1307          fakerepo = self.fake_repo(self.REPO)
  1308          subprocess.check_call(['git', 'init', fakerepo])
  1309          os.chdir(fakerepo)
  1310          subprocess.check_call(['git', 'config', 'user.name', 'foo'])
  1311          subprocess.check_call(['git', 'config', 'user.email', 'foo@bar.baz'])
  1312          subprocess.check_call(['touch', self.MASTER])
  1313          subprocess.check_call(['git', 'add', self.MASTER])
  1314          subprocess.check_call(['cp', '-r', bootstrap.test_infra('jenkins/fake'), fakerepo])
  1315          subprocess.check_call(['git', 'add', 'fake'])
  1316          subprocess.check_call(['git', 'commit', '-m', 'Initial commit'])
  1317          subprocess.check_call(['git', 'checkout', 'master'])
  1318  
  1319      def tearDown(self):
  1320          for stub in self.boiler:
  1321              with stub:  # Leaving with restores things
  1322                  pass
  1323          os.chdir(self.ocwd)
  1324          subprocess.check_call(['rm', '-rf', self.root_github])
  1325          subprocess.check_call(['rm', '-rf', self.root_workspace])
  1326          subprocess.check_call(['rm', '-rf', self.root_git_cache])
  1327  
  1328      def test_git_cache(self):
  1329          subprocess.check_call(['git', 'checkout', '-b', self.BRANCH])
  1330          subprocess.check_call(['git', 'rm', self.MASTER])
  1331          subprocess.check_call(['touch', self.BRANCH_FILE])
  1332          subprocess.check_call(['git', 'add', self.BRANCH_FILE])
  1333          subprocess.check_call(['git', 'commit', '-m', 'Create %s' % self.BRANCH])
  1334          test_bootstrap(
  1335              job='fake-branch',
  1336              repo=self.REPO,
  1337              branch=self.BRANCH,
  1338              pull=None,
  1339              root=self.root_workspace,
  1340              git_cache=self.root_git_cache)
  1341          # Verify that the cache was populated by running a simple git command
  1342          # in the git cache directory.
  1343          subprocess.check_call(
  1344              ['git', '--git-dir=%s/%s' % (self.root_git_cache, self.REPO), 'log'])
  1345  
  1346      def test_pr(self):
  1347          subprocess.check_call(['git', 'checkout', 'master'])
  1348          subprocess.check_call(['git', 'checkout', '-b', 'unknown-pr-branch'])
  1349          subprocess.check_call(['git', 'rm', self.MASTER])
  1350          subprocess.check_call(['touch', self.PR_FILE])
  1351          subprocess.check_call(['git', 'add', self.PR_FILE])
  1352          subprocess.check_call(['git', 'commit', '-m', 'Create branch for PR %d' % self.PR_NUM])
  1353          subprocess.check_call(['git', 'tag', self.PR_TAG])
  1354          os.chdir('/tmp')
  1355          test_bootstrap(
  1356              job='fake-pr',
  1357              repo=self.REPO,
  1358              branch=None,
  1359              pull=self.PR_NUM,
  1360              root=self.root_workspace)
  1361  
  1362      def test_branch(self):
  1363          subprocess.check_call(['git', 'checkout', '-b', self.BRANCH])
  1364          subprocess.check_call(['git', 'rm', self.MASTER])
  1365          subprocess.check_call(['touch', self.BRANCH_FILE])
  1366          subprocess.check_call(['git', 'add', self.BRANCH_FILE])
  1367          subprocess.check_call(['git', 'commit', '-m', 'Create %s' % self.BRANCH])
  1368  
  1369          os.chdir('/tmp')
  1370          test_bootstrap(
  1371              job='fake-branch',
  1372              repo=self.REPO,
  1373              branch=self.BRANCH,
  1374              pull=None,
  1375              root=self.root_workspace)
  1376  
  1377      def test_branch_ref(self):
  1378          """Make sure we check out a specific commit."""
  1379          subprocess.check_call(['git', 'checkout', '-b', self.BRANCH])
  1380          subprocess.check_call(['git', 'rm', self.MASTER])
  1381          subprocess.check_call(['touch', self.BRANCH_FILE])
  1382          subprocess.check_call(['git', 'add', self.BRANCH_FILE])
  1383          subprocess.check_call(['git', 'commit', '-m', 'Create %s' % self.BRANCH])
  1384          sha = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip()
  1385          subprocess.check_call(['rm', self.BRANCH_FILE])
  1386          subprocess.check_call(['git', 'add', self.BRANCH_FILE])
  1387          subprocess.check_call(['git', 'commit', '-m', 'Delete %s' % self.BRANCH])
  1388  
  1389          os.chdir('/tmp')
  1390          # Supplying the commit exactly works.
  1391          test_bootstrap(
  1392              job='fake-branch',
  1393              repo=self.REPO,
  1394              branch='%s:%s' % (self.BRANCH, sha),
  1395              pull=None,
  1396              root=self.root_workspace)
  1397          # Supplying the commit through repo works.
  1398          test_bootstrap(
  1399              job='fake-branch',
  1400              repo="%s=%s:%s" % (self.REPO, self.BRANCH, sha),
  1401              branch=None,
  1402              pull=None,
  1403              root=self.root_workspace)
  1404          # Using branch head fails.
  1405          with self.assertRaises(SystemExit):
  1406              test_bootstrap(
  1407                  job='fake-branch',
  1408                  repo=self.REPO,
  1409                  branch=self.BRANCH,
  1410                  pull=None,
  1411                  root=self.root_workspace)
  1412          with self.assertRaises(SystemExit):
  1413              test_bootstrap(
  1414                  job='fake-branch',
  1415                  repo="%s=%s" % (self.REPO, self.BRANCH),
  1416                  branch=None,
  1417                  pull=None,
  1418                  root=self.root_workspace)
  1419  
  1420      def test_batch(self):
  1421          def head_sha():
  1422              # We can't hardcode the SHAs for the test, so we need to determine
  1423              # them after each commit.
  1424              return subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip()
  1425          refs = ['master:%s' % head_sha()]
  1426          master_commit_date = int(subprocess.check_output(
  1427              ['git', 'show', '-s', '--format=format:%ct', head_sha()]))
  1428          for pr in (123, 456):
  1429              subprocess.check_call(['git', 'checkout', '-b', 'refs/pull/%d/head' % pr, 'master'])
  1430              subprocess.check_call(['git', 'rm', self.MASTER])
  1431              subprocess.check_call(['touch', self.PR_FILE])
  1432              subprocess.check_call(['git', 'add', self.PR_FILE])
  1433              open('pr_%d.txt' % pr, 'w').write('some text')
  1434              subprocess.check_call(['git', 'add', 'pr_%d.txt' % pr])
  1435              subprocess.check_call(['git', 'commit', '-m', 'add some stuff (#%d)' % pr])
  1436              refs.append('%d:%s' % (pr, head_sha()))
  1437          os.chdir('/tmp')
  1438          pull = ','.join(refs)
  1439          print '--pull', pull
  1440          subprocess.check_call(['ls'])
  1441          test_bootstrap(
  1442              job='fake-pr',
  1443              repo=self.REPO,
  1444              branch=None,
  1445              pull=pull,
  1446              root=self.root_workspace)
  1447          head_commit_date = int(subprocess.check_output(
  1448              ['git', 'show', '-s', '--format=format:%ct', 'test']))
  1449          # Since there were 2 PRs merged, we expect the timestamp of the latest
  1450          # commit on the 'test' branch to be 2 more than master.
  1451          self.assertEqual(head_commit_date, master_commit_date + 2)
  1452  
  1453      def test_pr_bad(self):
  1454          random_pr = 111
  1455          with Stub(bootstrap, 'start', Bomb):
  1456              with Stub(time, 'sleep', Pass):
  1457                  with self.assertRaises(subprocess.CalledProcessError):
  1458                      test_bootstrap(
  1459                          job='fake-pr',
  1460                          repo=self.REPO,
  1461                          branch=None,
  1462                          pull=random_pr,
  1463                          root=self.root_workspace)
  1464  
  1465      def test_branch_bad(self):
  1466          random_branch = 'something'
  1467          with Stub(bootstrap, 'start', Bomb):
  1468              with Stub(time, 'sleep', Pass):
  1469                  with self.assertRaises(subprocess.CalledProcessError):
  1470                      test_bootstrap(
  1471                          job='fake-branch',
  1472                          repo=self.REPO,
  1473                          branch=random_branch,
  1474                          pull=None,
  1475                          root=self.root_workspace)
  1476  
  1477      def test_job_missing(self):
  1478          with self.assertRaises(ValueError):
  1479              test_bootstrap(
  1480                  job='this-job-no-exists',
  1481                  repo=self.REPO,
  1482                  branch='master',
  1483                  pull=None,
  1484                  root=self.root_workspace)
  1485  
  1486      def test_job_fails(self):
  1487          with self.assertRaises(SystemExit):
  1488              test_bootstrap(
  1489                  job='fake-failure',
  1490                  repo=self.REPO,
  1491                  branch='master',
  1492                  pull=None,
  1493                  root=self.root_workspace)
  1494  
  1495      def test_commit_in_meta(self):
  1496          sha = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip()
  1497          cwd = os.getcwd()
  1498          os.chdir(bootstrap.test_infra('.'))
  1499          infra_sha = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip()[:9]
  1500          os.chdir(cwd)
  1501  
  1502          # Commit SHA should in meta
  1503          call = lambda *a, **kw: bootstrap._call(5, *a, **kw)
  1504          repos = repo({REPO: ('master', ''), 'other-repo': ('other-branch', '')})
  1505          meta = bootstrap.metadata(repos, 'missing-artifacts-dir', call)
  1506          self.assertIn('repo-commit', meta)
  1507          self.assertEquals(sha, meta['repo-commit'])
  1508          self.assertEquals(40, len(meta['repo-commit']))
  1509          self.assertIn('infra-commit', meta)
  1510          self.assertEquals(infra_sha, meta['infra-commit'])
  1511          self.assertEquals(9, len(meta['infra-commit']))
  1512          self.assertIn(REPO, meta.get('repos'))
  1513          self.assertIn('other-repo', meta.get('repos'))
  1514          self.assertEquals(REPO, meta.get('repo'))
  1515  
  1516  
  1517  class ParseArgsTest(unittest.TestCase):
  1518      def test_barerepo_both(self):
  1519          with self.assertRaises(argparse.ArgumentTypeError):
  1520              bootstrap.parse_args(['--bare', '--repo=hello', '--job=j'])
  1521  
  1522      def test_barerepo_neither(self):
  1523          with self.assertRaises(argparse.ArgumentTypeError):
  1524              bootstrap.parse_args(['--job=j'])
  1525  
  1526      def test_barerepo_bareonly(self):
  1527          args = bootstrap.parse_args(['--bare', '--job=j'])
  1528          self.assertFalse(args.repo, args)
  1529          self.assertTrue(args.bare, args)
  1530  
  1531      def test_barerepo_repoonly(self):
  1532          args = bootstrap.parse_args(['--repo=R', '--job=j'])
  1533          self.assertFalse(args.bare, args)
  1534          self.assertTrue(args.repo, args)
  1535  
  1536  
  1537      def test_extra_job_args(self):
  1538          args = bootstrap.parse_args(['--repo=R', '--job=j', '--', '--foo=bar', '--baz=quux'])
  1539          self.assertEquals(args.extra_job_args, ['--foo=bar', '--baz=quux'])
  1540  
  1541  
  1542  if __name__ == '__main__':
  1543      unittest.main()