1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import os
7import re
8import subprocess
9import sys
10import tarfile
11import tempfile
12import test_server
13import unittest
14import zipfile
15
16SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
17BUILD_TOOLS_DIR = os.path.dirname(SCRIPT_DIR)
18TOOLS_DIR = os.path.join(os.path.dirname(BUILD_TOOLS_DIR), 'tools')
19
20sys.path.extend([BUILD_TOOLS_DIR, TOOLS_DIR])
21import getos
22import manifest_util
23import oshelpers
24
25
26MANIFEST_BASENAME = 'naclsdk_manifest2.json'
27
28# Attribute '' defined outside __init__
29# pylint: disable=W0201
30
31class SdkToolsTestCase(unittest.TestCase):
32  def tearDown(self):
33    if self.server:
34      self.server.Shutdown()
35    oshelpers.Remove(['-rf', self.basedir])
36
37  def SetupDefault(self):
38    self.SetupWithBaseDirPrefix('sdktools')
39
40  def SetupWithBaseDirPrefix(self, basedir_prefix, tmpdir=None):
41    self.basedir = tempfile.mkdtemp(prefix=basedir_prefix, dir=tmpdir)
42    self.cache_dir = os.path.join(self.basedir, 'nacl_sdk', 'sdk_cache')
43    # We have to make sure that we build our updaters with a version that is at
44    # least as large as the version in the sdk_tools bundle. If not, update
45    # tests may fail because the "current" version (according to the sdk_cache)
46    # is greater than the version we are attempting to update to.
47    self.current_revision = self._GetSdkToolsBundleRevision()
48    self._BuildUpdater(self.basedir, self.current_revision)
49    self.manifest = self._ReadCacheManifest()
50    self.sdk_tools_bundle = self.manifest.GetBundle('sdk_tools')
51    self.server = test_server.LocalHTTPServer(self.basedir)
52
53  def _GetSdkToolsBundleRevision(self):
54    """Get the sdk_tools bundle revision.
55    We get this from the checked-in path; this is the same file that
56    build_updater uses to specify the current revision of sdk_tools."""
57
58    manifest_filename = os.path.join(BUILD_TOOLS_DIR, 'json',
59                                     'naclsdk_manifest0.json')
60    manifest = manifest_util.SDKManifest()
61    manifest.LoadDataFromString(open(manifest_filename, 'r').read())
62    return manifest.GetBundle('sdk_tools').revision
63
64  def _WriteConfig(self, config_data):
65    config_filename = os.path.join(self.cache_dir, 'naclsdk_config.json')
66    with open(config_filename, 'w') as stream:
67      stream.write(config_data)
68
69  def _WriteCacheManifest(self, manifest):
70    """Write the manifest at nacl_sdk/sdk_cache.
71
72    This is useful for faking having installed a bundle.
73    """
74    manifest_filename = os.path.join(self.cache_dir, MANIFEST_BASENAME)
75    with open(manifest_filename, 'w') as stream:
76      stream.write(manifest.GetDataAsString())
77
78  def _ReadCacheManifest(self):
79    """Read the manifest at nacl_sdk/sdk_cache."""
80    manifest_filename = os.path.join(self.cache_dir, MANIFEST_BASENAME)
81    manifest = manifest_util.SDKManifest()
82    with open(manifest_filename) as stream:
83      manifest.LoadDataFromString(stream.read())
84    return manifest
85
86  def _WriteManifest(self):
87    with open(os.path.join(self.basedir, MANIFEST_BASENAME), 'w') as stream:
88      stream.write(self.manifest.GetDataAsString())
89
90  def _BuildUpdater(self, out_dir, revision=None):
91    build_updater_py = os.path.join(BUILD_TOOLS_DIR, 'build_updater.py')
92    cmd = [sys.executable, build_updater_py, '-o', out_dir]
93    if revision:
94      cmd.extend(['-r', str(revision)])
95
96    process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
97    _, _ = process.communicate()
98    self.assertEqual(process.returncode, 0)
99
100  def _BuildUpdaterArchive(self, rel_path, revision):
101    """Build a new sdk_tools bundle.
102
103    Args:
104      rel_path: The relative path to build the updater.
105      revision: The revision number to give to this bundle.
106    Returns:
107      A manifest_util.Archive() that points to this new bundle on the local
108      server.
109    """
110    self._BuildUpdater(os.path.join(self.basedir, rel_path), revision)
111
112    new_sdk_tools_tgz = os.path.join(self.basedir, rel_path, 'sdk_tools.tgz')
113    with open(new_sdk_tools_tgz, 'rb') as sdk_tools_stream:
114      archive_sha1, archive_size = manifest_util.DownloadAndComputeHash(
115          sdk_tools_stream)
116
117    archive = manifest_util.Archive('all')
118    archive.url = self.server.GetURL('%s/sdk_tools.tgz' % (rel_path,))
119    archive.checksum = archive_sha1
120    archive.size = archive_size
121    return archive
122
123  def _Run(self, args, expect_error=False):
124    naclsdk_shell_script = os.path.join(self.basedir, 'nacl_sdk', 'naclsdk')
125    if getos.GetPlatform() == 'win':
126      naclsdk_shell_script += '.bat'
127    cmd = [naclsdk_shell_script]
128    cmd.extend(args)
129    cmd.extend(['-U', self.server.GetURL(MANIFEST_BASENAME)])
130    process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
131    stdout, _ = process.communicate()
132
133    if ((expect_error and process.returncode == 0) or
134        (not expect_error and process.returncode != 0)):
135      self.fail('Error running nacl_sdk:\n"""\n%s\n"""' % stdout)
136
137    return stdout
138
139  def _RunAndExtractRevision(self):
140    stdout = self._Run(['version'])
141    match = re.search('version r(\d+)', stdout)
142    self.assertTrue(match is not None)
143    return int(match.group(1))
144
145
146class TestSdkTools(SdkToolsTestCase):
147  def testPathHasSpaces(self):
148    """Test that running naclsdk from a path with spaces works."""
149    self.SetupWithBaseDirPrefix('sdk tools')
150    self._WriteManifest()
151    self._RunAndExtractRevision()
152
153
154class TestBuildUpdater(SdkToolsTestCase):
155  def setUp(self):
156    self.SetupDefault()
157
158  def testUpdaterPathsAreSane(self):
159    """Test that the paths to files in nacl_sdk.zip and sdktools.tgz are
160    relative to the output directory."""
161    nacl_sdk_zip_path = os.path.join(self.basedir, 'nacl_sdk.zip')
162    zip_stream = zipfile.ZipFile(nacl_sdk_zip_path, 'r')
163    try:
164      self.assertTrue(all(name.startswith('nacl_sdk')
165                          for name in zip_stream.namelist()))
166    finally:
167      zip_stream.close()
168
169    # sdktools.tgz has no built-in directories to look for. Instead, just look
170    # for some files that must be there.
171    sdktools_tgz_path = os.path.join(self.basedir, 'sdk_tools.tgz')
172    tar_stream = tarfile.open(sdktools_tgz_path, 'r:gz')
173    try:
174      names = [m.name for m in tar_stream.getmembers()]
175      self.assertTrue('LICENSE' in names)
176      self.assertTrue('sdk_update.py' in names)
177    finally:
178      tar_stream.close()
179
180
181class TestAutoUpdateSdkTools(SdkToolsTestCase):
182  def setUp(self):
183    self.SetupDefault()
184
185  def testNoUpdate(self):
186    """Test that running naclsdk with current revision does nothing."""
187    self._WriteManifest()
188    revision = self._RunAndExtractRevision()
189    self.assertEqual(revision, self.current_revision)
190
191  def testUpdate(self):
192    """Test that running naclsdk with a new revision will auto-update."""
193    new_revision = self.current_revision + 1
194    archive = self._BuildUpdaterArchive('new', new_revision)
195    self.sdk_tools_bundle.RemoveAllArchivesForHostOS(archive.host_os)
196    self.sdk_tools_bundle.AddArchive(archive)
197    self.sdk_tools_bundle.revision = new_revision
198    self._WriteManifest()
199
200    revision = self._RunAndExtractRevision()
201    self.assertEqual(revision, new_revision)
202
203  def testManualUpdateIsIgnored(self):
204    """Test that attempting to manually update sdk_tools is ignored.
205
206    If the sdk_tools bundle was updated normally (i.e. the old way), it would
207    leave a sdk_tools_update folder that would then be copied over on a
208    subsequent run. This test ensures that there is no folder made.
209    """
210    new_revision = self.current_revision + 1
211    archive = self._BuildUpdaterArchive('new', new_revision)
212    self.sdk_tools_bundle.RemoveAllArchivesForHostOS(archive.host_os)
213    self.sdk_tools_bundle.AddArchive(archive)
214    self.sdk_tools_bundle.revision = new_revision
215    self._WriteManifest()
216
217    sdk_tools_update_dir = os.path.join(self.basedir, 'nacl_sdk',
218        'sdk_tools_update')
219    self.assertFalse(os.path.exists(sdk_tools_update_dir))
220    stdout = self._Run(['update', 'sdk_tools'])
221    self.assertTrue(stdout.find('Ignoring manual update request.') != -1)
222    self.assertFalse(os.path.exists(sdk_tools_update_dir))
223
224  def testHelpCommand(self):
225    """Running naclsdk with -h should work.
226
227    This is a regression test for a bug where the auto-updater would remove the
228    sdk_tools directory when running "naclsdk -h".
229    """
230    self._WriteManifest()
231    self._Run(['-h'])
232
233
234class TestAutoUpdateSdkToolsDifferentFilesystem(TestAutoUpdateSdkTools):
235  def setUp(self):
236    # On Linux (on my machine at least), /tmp is a different filesystem than
237    # the current directory. os.rename fails when the source and destination
238    # are on different filesystems. Test that case here.
239    self.SetupWithBaseDirPrefix('sdktools', tmpdir='.')
240
241
242if __name__ == '__main__':
243  sys.exit(unittest.main())
244