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