core.py 13.3 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :

###################################################################################
#                                                                                 #
# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/               #
# Contact: beat.support@idiap.ch                                                  #
#                                                                                 #
# Redistribution and use in source and binary forms, with or without              #
# modification, are permitted provided that the following conditions are met:     #
#                                                                                 #
# 1. Redistributions of source code must retain the above copyright notice, this  #
# list of conditions and the following disclaimer.                                #
#                                                                                 #
# 2. Redistributions in binary form must reproduce the above copyright notice,    #
# this list of conditions and the following disclaimer in the documentation       #
# and/or other materials provided with the distribution.                          #
#                                                                                 #
# 3. Neither the name of the copyright holder nor the names of its contributors   #
# may be used to endorse or promote products derived from this software without   #
# specific prior written permission.                                              #
#                                                                                 #
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND #
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED   #
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE          #
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE    #
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL      #
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR      #
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER      #
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,   #
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE   #
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.            #
#                                                                                 #
###################################################################################


"""
Base class for asset testing
"""

41
import os
42
43
import nose.tools
import click
44
import shutil
45
from collections import namedtuple
46

47
48
49
from click.testing import CliRunner

from beat.core.test.utils import cleanup
50
from beat.core.test.utils import skipif
51
from beat.core.test.utils import slow
52
53
54
55
from beat.cmdline.scripts import main_cli

from .. import common

56
57
58
59
60
61
62
63
64
65
66
67
68
69
from . import platform, disconnected, prefix, tmp_prefix, user, token

if not disconnected:
    from django.contrib.staticfiles.testing import LiveServerTestCase
else:

    class LiveServerTestCase:
        """Dummy shell class"""

        live_server_url = None

        @classmethod
        def setUpClass(cls):
            pass
70
71


72
73
74
# Make skip on disconnected a decorator, this will make tests easier to read and write
skip_disconnected = skipif(disconnected, "missing test platform (%s)" % platform)

75
76
77
# Used for making direct calls
MockConfig = namedtuple("MockConfig", ["platform", "user", "token"])

78

79
class BaseTest:
80
81
    asset_type = None

82
83
84
85
    def setUp(self):
        pass

    def tearDown(self):
86
87
        cleanup()

88
89
90
91
92
93
94
95
96
97
98
    @classmethod
    def get_cmd_group(cls, asset_type):
        try:
            cmd_group = common.TYPE_PLURAL[asset_type]
        except KeyError:
            return asset_type

        if "/" in cmd_group:
            cmd_group = cmd_group.split("/")[-1]
        return cmd_group

99
100
    @classmethod
    def call(cls, *args, **kwargs):
101
102
103
104
105
        """A central mechanism to call the main routine with the right parameters"""

        use_prefix = kwargs.get("prefix", prefix)
        use_platform = kwargs.get("platform", platform)
        use_cache = kwargs.get("cache", "cache")
106
        asset_type = kwargs.get("asset_type", cls.asset_type)
107

108
        cmd_group = cls.get_cmd_group(asset_type)
109

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
        parameters = [
            "--test-mode",
            "--prefix",
            use_prefix,
            "--token",
            token,
            "--user",
            user,
            "--cache",
            use_cache,
            "--platform",
            use_platform,
        ]

        if cmd_group:
            parameters.append(cmd_group)

        parameters += list(args)

129
130
        runner = CliRunner()
        with runner.isolated_filesystem():
131
            result = runner.invoke(main_cli.main, parameters, catch_exceptions=False)
132
133
134
135
136

        if result.exit_code != 0:
            click.echo(result.output)
        return result.exit_code, result.output

137

138
139
140
class AssetBaseTest(BaseTest):
    """Base class that ensures that the asset_type is set before calling click"""

141
142
143
144
145
146
147
148
149
150
151
152
    object_map = {}
    storage_cls = None

    @classmethod
    def create(cls, obj=None):
        obj = obj or cls.object_map["create"]
        exit_code, outputs = cls.call("create", obj, prefix=tmp_prefix)
        nose.tools.eq_(exit_code, 0, outputs)
        storage = cls.storage_cls(tmp_prefix, obj)
        nose.tools.assert_true(storage.exists())
        return storage

153
154
155
156
157
158
159
    @classmethod
    def delete(cls, obj):
        exit_code, outputs = cls.call("rm", obj, prefix=tmp_prefix)
        nose.tools.eq_(exit_code, 0, outputs)
        storage = cls.storage_cls(tmp_prefix, obj)
        nose.tools.assert_false(storage.exists())

160
161
162
163
164
165
    @classmethod
    def call(cls, *args, **kwargs):
        nose.tools.assert_is_not_none(cls.asset_type, "Missing value for asset_type")
        return super().call(*args, **kwargs)


166
class AssetLocalTest(AssetBaseTest):
167
    """Base class for local tests"""
168
169
170
171

    def __init__(self):
        super().__init__()
        nose.tools.assert_true(self.object_map)
172
        nose.tools.assert_is_not_none(self.storage_cls)
173

174
175
176
177
178
179
180
181
182
183
184
185
    def test_local_list(self):
        exit_code, outputs = self.call("list")
        nose.tools.eq_(exit_code, 0, outputs)

    def test_check_valid(self):
        exit_code, outputs = self.call("check", self.object_map["valid"])
        nose.tools.eq_(exit_code, 0, outputs)

    def test_check_invalid(self):
        exit_code, outputs = self.call("check", self.object_map["invalid"])
        nose.tools.eq_(exit_code, 1, outputs)

186
187
188
    def test_create(self, obj=None):
        self.create(self.object_map["create"])

189
190
191
    def test_new_version(self):
        obj = self.object_map["create"]
        obj2 = self.object_map["new"]
192
        self.create(obj)
193
194
195
196
197
198
199
200
201
202
203
204
        exit_code, outputs = self.call("version", obj, prefix=tmp_prefix)
        nose.tools.eq_(exit_code, 0, outputs)
        s = self.storage_cls(tmp_prefix, obj2)
        nose.tools.assert_true(s.exists())

        # check version status
        with common.Selector(tmp_prefix) as selector:
            nose.tools.eq_(selector.version_of(self.asset_type, obj2), obj)

    def test_fork(self):
        obj = self.object_map["create"]
        obj2 = self.object_map["fork"]
205
        self.create(obj)
206
        with common.Selector(tmp_prefix) as selector:
207
208
209
210
211
212
213
214
215
216
            if selector.can_fork(self.asset_type):
                exit_code, outputs = self.call("fork", obj, obj2, prefix=tmp_prefix)
                nose.tools.eq_(exit_code, 0, outputs)
                selector.load()
                s = self.storage_cls(tmp_prefix, obj2)
                nose.tools.assert_true(s.exists())
                nose.tools.eq_(selector.forked_from(self.asset_type, obj2), obj)
            else:
                exit_code, outputs = self.call("fork", obj, obj2, prefix=tmp_prefix)
                nose.tools.assert_not_equal(exit_code, 0)
217
218
219

    def test_delete_local(self):
        obj = self.object_map["create"]
220
221
        self.create(obj)
        self.delete(obj)
222
223
224
225
226
227
228
229
230

    def test_delete_local_unexisting(self):
        obj = self.object_map["create"]
        storage = self.storage_cls(tmp_prefix, obj)
        nose.tools.assert_false(storage.exists())

        exit_code, outputs = self.call("rm", obj, prefix=tmp_prefix)
        nose.tools.eq_(exit_code, 1, outputs)
        nose.tools.assert_false(storage.exists())
231
232


233
class AssetRemoteTest(AssetBaseTest):
234
    """Base class for remote tests"""
235
236
237
238

    def __init__(self):
        super().__init__()
        nose.tools.assert_true(self.object_map)
239
        nose.tools.assert_is_not_none(self.storage_cls)
240

241
242
243
244
    def _modify_asset(self, asset_name):
        """Modify an asset"""
        raise NotImplementedError

245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
    @slow
    @skip_disconnected
    def test_remote_list(self):
        exit_code, output = self.call("list", "--remote")
        nose.tools.eq_(exit_code, 0, output)

    @slow
    @skip_disconnected
    def test_pull_one(self, obj=None):
        obj = obj or self.object_map["pull"]
        exit_code, output = self.call("pull", obj, prefix=tmp_prefix)
        nose.tools.eq_(exit_code, 0, output)
        storage = self.storage_cls(tmp_prefix, obj)
        nose.tools.assert_true(storage.exists())
        return storage

    @slow
    @skip_disconnected
    def test_pull_all(self):
        exit_code, output = self.call("pull", prefix=tmp_prefix)
        nose.tools.eq_(exit_code, 0, output)

267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
    @slow
    @skip_disconnected
    def test_diff(self):
        obj = self.object_map["diff"]
        exit_code, output = self.call("pull", obj, prefix=tmp_prefix)
        nose.tools.eq_(exit_code, 0, output)

        # quickly modify the user library by emptying it
        self._modify_asset(obj)

        exit_code, output = self.call("diff", obj, prefix=tmp_prefix)
        nose.tools.eq_(exit_code, 0, output)

    @slow
    @skip_disconnected
    def test_status(self):
        self.test_diff()
        self.test_pull_one()
        exit_code, output = self.call("status", prefix=tmp_prefix)
        nose.tools.eq_(exit_code, 0, output)

288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
    @slow
    @skip_disconnected
    def test_push_different_versions(self):
        with common.Selector(tmp_prefix) as selector:
            if not selector.has_versions(self.asset_type):
                raise nose.SkipTest(
                    "{} does not support versions".format(self.asset_type)
                )

        asset_name = self.object_map["create"]
        self.create(asset_name)

        number_of_versions = 5
        version_pos = asset_name.rindex("/") + 1
        original_name = asset_name[:version_pos]
        original_version = int(asset_name[version_pos:])

        for i in range(number_of_versions):
            asset_name = original_name + str(original_version + i)
            exit_code, outputs = self.call("version", asset_name, prefix=tmp_prefix)
            nose.tools.eq_(exit_code, 0, outputs)

        asset_name = original_name + str(original_version + number_of_versions)

        exit_code, output = self.call("push", asset_name, prefix=tmp_prefix)

        nose.tools.eq_(exit_code, 0, output)

        config = MockConfig(self.live_server_url, user, token)
        with common.make_webapi(config) as webapi:
            asset_list = common.retrieve_remote_list(webapi, self.asset_type, ["name"])
            aoi_list = [
                asset for asset in asset_list if asset["name"].startswith(original_name)
            ]
            nose.tools.assert_equal(len(aoi_list), number_of_versions + 1)

324
325
326
    @slow
    @skip_disconnected
    def test_push_and_delete(self):
Samuel GAIST's avatar
Samuel GAIST committed
327
        asset_name = self.object_map["push"]
328
329

        # now push the new object and then delete it remotely
Samuel GAIST's avatar
Samuel GAIST committed
330
        exit_code, output = self.call("push", asset_name)
331
        nose.tools.eq_(exit_code, 0, output)
Samuel GAIST's avatar
Samuel GAIST committed
332
        exit_code, output = self.call("rm", "--remote", asset_name)
333
334
        nose.tools.eq_(exit_code, 0, output)

Samuel GAIST's avatar
Samuel GAIST committed
335
336
337
338
339
340
341
342
    @slow
    @skip_disconnected
    def test_fail_not_owner_push(self):
        asset_name = self.object_map["not_owner_push"]

        exit_code, output = self.call("push", asset_name)
        nose.tools.eq_(exit_code, 1, output)

343

344
345
class OnlineTestMixin:
    """Mixin for using Django's live server"""
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364

    def setUp(self):
        """Cache a copy of the database to avoid the need to call make install
        on each tests.
        """

        if not disconnected:
            from django.conf import settings

            database_path = settings.DATABASES["default"]["TEST"]["NAME"]
            db_backup = os.path.join(prefix, "django_test_database.sqlite3")

            if not os.path.exists(db_backup):
                shutil.copyfile(database_path, db_backup)
            else:
                shutil.copyfile(db_backup, database_path)

    @classmethod
    def call(cls, *args, **kwargs):
365
        """Re-implement for platform URL handling"""
366

367
        kwargs["platform"] = cls.live_server_url
368

369
        return super().call(*args, **kwargs)
370
371
372
373
374
375
376
377
378
379


class OnlineTestCase(LiveServerTestCase, OnlineTestMixin, BaseTest):
    """Test case using django live server for test of remote functions"""

    def setUp(self):
        for base in OnlineTestCase.__bases__:
            base.setUp(self)


380
class OnlineAssetTestCase(LiveServerTestCase, OnlineTestMixin, AssetRemoteTest):
381
382
383
384
385
    """Test case using django live server for asset related remote tests"""

    def setUp(self):
        for base in OnlineTestCase.__bases__:
            base.setUp(self)