-
Notifications
You must be signed in to change notification settings - Fork 126
/
Copy pathtest_manifest.py
2897 lines (2532 loc) · 90 KB
/
test_manifest.py
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
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
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
863
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2018 Foundries.io Ltd
# Copyright (c) 2020, Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0
# Tests for the west.manifest API.
#
# Generally try to avoid shelling out to git in this test file, but if
# it's particularly inconvenient to test something without a real git
# repository, go ahead and make one in a temporary directory.
from copy import deepcopy
from glob import glob
import os
from pathlib import PurePath, Path
import platform
import subprocess
from unittest.mock import patch
import textwrap
import pytest
import yaml
from west.manifest import Manifest, Project, ManifestProject, \
MalformedManifest, ManifestVersionError, ManifestImportFailed, \
manifest_path, ImportFlag, validate, MANIFEST_PROJECT_INDEX, \
_ManifestImportDepth, is_group, SCHEMA_VERSION
# White box checks for the schema version.
from west.manifest import _VALID_SCHEMA_VERS
from conftest import create_workspace, create_repo, checkout_branch, \
create_branch, add_commit, add_tag, rev_parse, GIT, check_proj_consistency
assert 'TOXTEMPDIR' in os.environ, "you must run these tests using tox"
FPI = ImportFlag.FORCE_PROJECTS # to force project imports to use the callback
if platform.system() == 'Windows':
TOPDIR = 'C:\\topdir'
TOPDIR_POSIX = 'C:/topdir'
else:
TOPDIR = '/topdir'
TOPDIR_POSIX = TOPDIR
THIS_DIRECTORY = os.path.dirname(__file__)
@pytest.fixture
def tmp_workspace(tmpdir):
# This fixture creates a skeletal west workspace in a temporary
# directory on the file system, and changes directory there.
#
# If you use this fixture, you can create
# './mp/west.yml', then run tests using its contents using
# Manifest.from_file(), etc. Or just use manifest_repo().
# Create the manifest repository directory and skeleton config.
topdir = tmpdir / 'topdir'
create_workspace(topdir)
# Switch to the top-level west workspace directory,
# and give it to the test case.
topdir.chdir()
return topdir
@pytest.fixture
def manifest_repo(tmp_workspace):
# This creates a temporary manifest repository, changes directory
# to it, and returns a pathlike for it.
manifest_repo = tmp_workspace / 'mp'
create_repo(manifest_repo)
manifest_repo.topdir = Path(tmp_workspace)
return manifest_repo
def nodrive(path):
return os.path.splitdrive(path)[1]
def M(content, **kwargs):
# A convenience to save typing
return Manifest.from_data('manifest:\n' content, **kwargs)
def MF(**kwargs):
# A convenience to save typing
return Manifest.from_file(**kwargs)
#########################################
# The very basics
#
# We need to be able to instantiate Projects and parse manifest data
# from strings or dicts, as well as from the file system.
def test_project_init():
# Basic tests of the Project constructor and public attributes.
p = Project('p', 'some-url', revision='v1.2')
assert p.name == 'p'
assert p.url == 'some-url'
assert p.revision == 'v1.2'
assert p.path == 'p'
assert p.abspath is None
assert p.posixpath is None
assert p.clone_depth is None
assert p.west_commands == []
assert p.topdir is None
p = Project('p', 'some-url', clone_depth=4, west_commands='foo',
topdir=TOPDIR)
assert p.clone_depth == 4
assert p.west_commands == ['foo']
assert p.topdir == TOPDIR
assert p.abspath == os.path.join(TOPDIR, 'p')
assert p.posixpath == TOPDIR_POSIX '/p'
def test_manifest_from_data_without_topdir():
# We can load manifest data as a dict or a string.
# If no *topdir* argument is given, as is done here,
# absolute path attributes should be None.
manifest = Manifest.from_data('''\
manifest:
projects:
- name: foo
url: https://foo.com
''')
assert manifest.projects[-1].name == 'foo'
assert manifest.projects[-1].abspath is None
manifest = Manifest.from_data({'manifest':
{'projects':
[{'name': 'foo',
'url': 'https:foo.com'}]}})
assert manifest.projects[-1].name == 'foo'
assert manifest.projects[-1].abspath is None
def test_validate():
# Get some coverage for west.manifest.validate.
# White box
with pytest.raises(TypeError):
validate(None)
with pytest.raises(MalformedManifest):
validate('invalid')
with pytest.raises(MalformedManifest):
validate('not-a-manifest')
with pytest.raises(MalformedManifest):
validate({'not-manifest': 'foo'})
manifest_data = {'manifest': {'projects': [{'name': 'p', 'url': 'u'}]}}
assert validate(manifest_data) == manifest_data
with pytest.raises(MalformedManifest):
# White box:
#
# The 're' string in there is crafted specifically to force a
# yaml.scanner.ScannerError, which needs to be converted to
# MalformedManifest.
validate('''\
manifest:
projects:
- name: p
url: p-url
re
import: not-a-file
''')
assert validate('''\
manifest:
projects:
- name: p
url: u
''') == {
'manifest': {
'projects': [{'name': 'p', 'url': 'u'}]
}
}
def test_constructor_arg_validation():
with pytest.raises(ValueError) as e:
Manifest(source_data='x', topdir='y')
assert 'both topdir and source_data were given' in str(e.value)
with pytest.raises(ValueError) as e:
Manifest(source_data='x', config='y')
assert 'both source_data and config were given' in str(e.value)
#########################################
# Project parsing tests
#
# Tests for validating and parsing project data, including:
#
# - names
# - URLs
# - revisions
# - paths
# - clone depths
# - west commands
# - repr()
def test_projects_must_have_name():
# A project must have a name. Names must be unique.
with pytest.raises(MalformedManifest):
M('''\
projects:
- url: foo
''')
with pytest.raises(MalformedManifest):
M('''\
projects:
- name: foo
url: u1
- name: foo
url: u2
''')
m = M('''\
projects:
- name: foo
url: u1
- name: bar
url: u2
''')
assert m.projects[1].name == 'foo'
assert m.projects[2].name == 'bar'
def test_no_project_named_manifest():
# The name 'manifest' is reserved.
with pytest.raises(MalformedManifest):
M('''\
projects:
- name: manifest
url: u
''')
def test_project_named_west():
# A project named west is allowed now, even though it was once an error.
m = M('''\
projects:
- name: west
url: https://foo.com
''')
assert m.projects[1].name == 'west'
def test_project_urls():
# Projects must be initialized with a remote or a URL, but not both.
# The resulting URLs must behave as documented.
# The following cases are valid:
# - explicit url
# - explicit remote, no repo-path
# - explicit remote repo-path
# - default remote, no repo-path
# - default remote repo-path
ps = M('''\
defaults:
remote: r2
remotes:
- name: r1
url-base: https://foo.com
- name: r2
url-base: https://baz.com
projects:
- name: project1
url: https://bar.com/project1
- name: project2
remote: r1
- name: project3
remote: r1
repo-path: project3-path
- name: project4
- name: project5
repo-path: subdir/project-five
''').projects
assert ps[1].url == 'https://bar.com/project1'
assert ps[2].url == 'https://foo.com/project2'
assert ps[3].url == 'https://foo.com/project3-path'
assert ps[4].url == 'https://baz.com/project4'
assert ps[5].url == 'https://baz.com/subdir/project-five'
# A remotes section isn't required in a manifest if all projects
# are specified by URL.
ps = M('''\
projects:
- name: testproject
url: https://example.com/my-project
''').projects
assert ps[1].url == 'https://example.com/my-project'
# Projects can't have both url and remote attributes.
with pytest.raises(MalformedManifest):
M('''\
remotes:
- name: r1
url-base: https://example.com
projects:
- name: project1
remote: r1
url: https://example.com/project2
''')
# Projects can't combine url and repo-path.
with pytest.raises(MalformedManifest):
M('''\
projects:
- name: project1
repo-path: x
url: https://example.com/project2
''')
# A remote or URL must be given if no default remote is set.
with pytest.raises(MalformedManifest):
M('''\
remotes:
- name: r1
url-base: https://example.com
projects:
- name: project1
''')
# All remotes must be defined, even if there is a default.
with pytest.raises(MalformedManifest):
M('''\
defaults:
remote: r1
remotes:
- name: r1
url-base: https://example.com
projects:
- name: project1
remote: deadbeef
''')
def test_project_revisions():
# All projects have revisions.
# The default revision, if set, should take effect
# when not explicitly specified in a project.
m = M('''\
defaults:
revision: defaultrev
projects:
- name: p1
url: u1
- name: p2
url: u2
revision: rev
''')
expected = [Project('p1', 'u1', revision='defaultrev'),
Project('p2', 'u2', revision='rev')]
for p, e in zip(m.projects[1:], expected):
check_proj_consistency(p, e)
# The default revision, if not given in a defaults section, is
# master.
m = M('''\
projects:
- name: p1
url: u1
''')
assert m.projects[1].revision == 'master'
def test_project_paths_explicit_implicit():
# Test project path parsing.
# Project paths may be explicitly given, or implicit.
ps = M('''\
projects:
- name: p
url: u
path: foo
- name: q
url: u
''').projects
assert ps[1].path == 'foo'
assert ps[2].path == 'q'
def test_project_paths_absolute():
# Absolute path attributes should be None when loading from data.
ps = M('''\
remotes:
- name: testremote
url-base: https://example.com
projects:
- name: testproject
remote: testremote
path: sub/directory
''').projects
assert ps[1].path == 'sub/directory'
assert ps[1].abspath is None
assert ps[1].posixpath is None
def test_project_paths_unique():
# No two projects may have the same path.
with pytest.raises(MalformedManifest):
M('''\
projects:
- name: a
path: p
- name: p
''')
with pytest.raises(MalformedManifest):
M('''\
projects:
- name: a
path: p
- name: b
path: p
''')
def test_project_paths_with_repo_path():
# The same fetch URL may be checked out under two different
# names, as long as they end up in different places.
content = '''\
defaults:
remote: remote1
remotes:
- name: remote1
url-base: https://url1.com
projects:
- name: testproject_v1
revision: v1.0
repo-path: testproject
- name: testproject_v2
revision: v2.0
repo-path: testproject
'''
# Try this first without providing topdir.
m = M(content)
expected1 = Project('testproject_v1', 'https://url1.com/testproject',
revision='v1.0')
expected2 = Project('testproject_v2', 'https://url1.com/testproject',
revision='v2.0')
check_proj_consistency(m.projects[1], expected1)
check_proj_consistency(m.projects[2], expected2)
def test_project_clone_depth():
ps = M('''\
projects:
- name: foo
url: u1
- name: bar
url: u2
clone-depth: 4
''').projects
assert ps[1].clone_depth is None
assert ps[2].clone_depth == 4
def test_project_west_commands():
# Projects may also specify subdirectories with west commands.
m = M('''\
projects:
- name: zephyr
url: https://foo.com
west-commands: some-path/west-commands.yml
''')
assert m.projects[1].west_commands == ['some-path/west-commands.yml']
def test_project_git_methods(tmpdir):
# Test the internal consistency of the various methods that call
# out to git.
# Just manually create a Project instance. We don't need a full
# Manifest.
path = tmpdir / 'project'
p = Project('project', 'ignore-this-url', topdir=tmpdir)
# Helper for getting the contents of a.txt at a revision.
def a_content_at(rev):
return p.git(f'show {rev}:a.txt', capture_stderr=True,
capture_stdout=True).stdout.decode('ascii')
# The project isn't cloned yet.
assert not p.is_cloned()
# Create it, then verify the API knows it's cloned.
# Cache the current SHA.
create_repo(path)
assert p.is_cloned()
start_sha = p.sha('HEAD')
# If a.txt doesn't exist at a revision, we can't read it. If it
# does, we can.
with pytest.raises(subprocess.CalledProcessError):
a_content_at('HEAD')
add_commit(path, 'add a.txt', files={'a.txt': 'a'})
a_sha = p.sha('HEAD')
with pytest.raises(subprocess.CalledProcessError):
a_content_at(start_sha)
assert a_content_at(a_sha) == 'a'
# Checks for read_at() and listdir_at().
add_commit(path, 'add b.txt', files={'b.txt': 'b'})
b_sha = p.sha('HEAD')
assert p.read_at('a.txt', rev=a_sha) == b'a'
with pytest.raises(subprocess.CalledProcessError):
p.read_at('a.txt', rev=start_sha)
assert p.listdir_at('', rev=start_sha) == []
assert p.listdir_at('', rev=a_sha) == ['a.txt']
assert sorted(p.listdir_at('', rev=b_sha)) == ['a.txt', 'b.txt']
# p.git() should be able to take a cwd kwarg which is a PathLike
# or a str.
p.git('log -1', cwd=path)
p.git('log -1', cwd=str(path))
# Basic checks for functions which operate on commits.
assert a_content_at(a_sha) == 'a'
assert p.is_ancestor_of(start_sha, a_sha)
assert not p.is_ancestor_of(a_sha, start_sha)
assert p.is_up_to_date_with(start_sha)
assert p.is_up_to_date_with(a_sha)
assert p.is_up_to_date_with(b_sha)
p.revision = b_sha
assert p.is_up_to_date()
p.git(f'reset --hard {a_sha}')
assert not p.is_up_to_date()
def test_project_repr():
m = M('''\
projects:
- name: zephyr
url: https://foo.com
revision: r
west-commands: some-path/west-commands.yml
''')
assert repr(m.projects[1]) == \
'Project("zephyr", "https://foo.com", revision="r", path=\'zephyr\', clone_depth=None, west_commands=[\'some-path/west-commands.yml\'], topdir=None, groups=[], userdata=None)' # noqa: E501
def test_project_sha(tmpdir):
tmpdir = Path(os.fspath(tmpdir))
create_repo(tmpdir)
add_tag(tmpdir, 'test-tag')
expected_sha = rev_parse(tmpdir, 'HEAD^{commit}')
project = Project('name',
'url-do-not-fetch',
revision='test-tag',
path=tmpdir.name,
topdir=tmpdir.parent)
assert project.sha(project.revision) == expected_sha
def test_project_userdata(tmpdir):
m = M('''\
defaults:
remote: r
remotes:
- name: r
url-base: base
projects:
- name: foo
- name: bar
userdata: a-string
- name: baz
userdata:
key: value
''')
foo, bar, baz = m.get_projects(['foo', 'bar', 'baz'])
assert foo.userdata is None
assert bar.userdata == 'a-string'
assert baz.userdata == {'key': 'value'}
assert 'userdata' not in foo.as_dict()
assert 'a-string' == bar.as_dict()['userdata']
def test_self_userdata(tmpdir):
m = M('''
defaults:
remote: r
remotes:
- name: r
url-base: base
projects:
- name: bar
self:
path: foo
userdata:
key: value
''')
foo, bar = m.get_projects(['manifest', 'bar'])
assert m.userdata == {'key': 'value'}
assert foo.userdata == {'key': 'value'}
assert bar.userdata is None
assert 'userdata' in foo.as_dict()
assert 'userdata' not in bar.as_dict()
def test_self_missing_userdata(tmpdir):
m = M('''
defaults:
remote: r
remotes:
- name: r
url-base: base
projects:
- name: bar
self:
path: foo
''')
foo, bar = m.get_projects(['manifest', 'bar'])
assert m.userdata is None
assert foo.userdata is None
assert bar.userdata is None
assert 'userdata' not in foo.as_dict()
assert 'userdata' not in bar.as_dict()
def test_no_projects():
# An empty projects list is allowed.
m = Manifest.from_data('manifest: {}')
assert len(m.projects) == 1 # just ManifestProject
m = M('''
self:
path: foo
''')
assert len(m.projects) == 1 # just ManifestProject
#########################################
# Tests for the manifest repository
def test_manifest_project():
# Basic test that the manifest repository, when represented as a project,
# has attributes which make sense when loaded from data.
# Case 1: everything at defaults
m = M('''\
projects:
- name: name
url: url
''')
mp = m.projects[0]
assert mp.name == 'manifest'
assert mp.path is None
assert mp.topdir is None
assert mp.abspath is None
assert mp.posixpath is None
assert mp.url == ''
assert mp.revision == 'HEAD'
assert mp.clone_depth is None
# Case 2: path and west-commands are specified
m = M('''\
projects:
- name: name
url: url
self:
path: my-path
west-commands: cmds.yml
''')
mp = m.projects[0]
assert mp.name == 'manifest'
assert mp.path == 'my-path'
assert m.yaml_path == 'my-path'
assert mp.west_commands == ['cmds.yml']
assert mp.topdir is None
assert mp.abspath is None
assert mp.posixpath is None
assert mp.url == ''
assert mp.revision == 'HEAD'
assert mp.clone_depth is None
def test_self_tag():
# Manifests may contain a self section describing the manifest
# repository. It should work with multiple projects and remotes as
# expected.
m = M('''\
remotes:
- name: testremote1
url-base: https://example1.com
- name: testremote2
url-base: https://example2.com
projects:
- name: testproject1
remote: testremote1
revision: rev1
- name: testproject2
remote: testremote2
self:
path: the-manifest-path
west-commands: scripts/west_commands
''')
expected = [ManifestProject(path='the-manifest-path',
west_commands='scripts/west_commands'),
Project('testproject1', 'https://example1.com/testproject1',
revision='rev1'),
Project('testproject2', 'https://example2.com/testproject2')]
# Check the projects are as expected.
for p, e in zip(m.projects, expected):
check_proj_consistency(p, e)
# With a "self: path:" value, that will be available in the
# yaml_path attribute, but all other absolute and relative
# attributes are None since we aren't reading from a workspace.
assert m.abspath is None
assert m.relative_path is None
assert m.yaml_path == 'the-manifest-path'
assert m.repo_abspath is None
# If "self: path:" is missing, we won't have a yaml_path attribute.
m = M('''\
projects:
- name: p
url: u
''')
assert m.yaml_path is None
# Empty paths are an error.
with pytest.raises(MalformedManifest) as e:
M('''\
projects: []
self:
path:''')
assert 'must be nonempty if present' in str(e.value)
#########################################
# File system tests
#
# Parsing manifests from data is the base case that everything else
# reduces to, but parsing may also be done from files on the file
# system, or "as if" it were done from files on the file system.
def test_from_topdir(tmp_workspace):
# If you load from topdir along with some source data, you will
# get absolute paths.
#
# This is true of both projects and the manifest itself.
topdir = Path(str(tmp_workspace))
repo_abspath = topdir / 'mp'
relpath = Path('mp') / 'west.yml'
abspath = topdir / relpath
mf = topdir / relpath
# Case 1: manifest has no "self: path:".
with open(mf, 'w', encoding='utf-8') as f:
f.write('''
manifest:
projects:
- name: my-cool-project
url: from-manifest-dir
''')
m = Manifest.from_topdir(topdir=topdir)
# Path-related Manifest attribute tests.
assert Path(m.abspath) == mf
assert m.posixpath == mf.as_posix()
assert Path(m.relative_path) == relpath
assert m.yaml_path is None
assert Path(m.repo_abspath) == repo_abspath
assert m.repo_posixpath == repo_abspath.as_posix()
assert Path(m.topdir) == topdir
# Legacy ManifestProject tests.
mproj = m.projects[MANIFEST_PROJECT_INDEX]
assert Path(mproj.topdir) == topdir
assert Path(mproj.path) == Path('mp')
# Project tests.
p1 = m.projects[1]
assert Path(p1.topdir) == Path(topdir)
assert Path(p1.abspath) == Path(topdir / 'my-cool-project')
# Case 2: manifest has a "self: path:", which disagrees with the
# actual file system path.
with open(mf, 'w', encoding='utf-8') as f:
f.write('''
manifest:
projects:
- name: my-cool-project
url: from-manifest-dir
self:
path: something/else
''')
m = Manifest.from_topdir(topdir=topdir)
# Path-related Manifest attribute tests.
assert Path(m.abspath) == abspath
assert m.posixpath == abspath.as_posix()
assert Path(m.relative_path) == relpath
assert m.yaml_path == 'something/else'
assert Path(m.repo_abspath) == repo_abspath
assert m.repo_posixpath == repo_abspath.as_posix()
assert Path(m.topdir) == topdir
# Legacy ManifestProject tests.
mproj = m.projects[MANIFEST_PROJECT_INDEX]
assert Path(mproj.topdir).is_absolute()
assert Path(mproj.topdir) == topdir
assert Path(mproj.path) == Path('mp')
assert Path(mproj.abspath).is_absolute()
assert Path(mproj.abspath) == repo_abspath
# Project tests.
p1 = m.projects[1]
assert Path(p1.topdir) == Path(topdir)
assert Path(p1.abspath) == topdir / 'my-cool-project'
# Case 3: project has a path. This always takes effect.
with open(mf, 'w', encoding='utf-8') as f:
f.write('''
manifest:
projects:
- name: my-cool-project
url: from-manifest-dir
path: project-path
self:
path: something/else
''')
m = Manifest.from_topdir(topdir=topdir)
p1 = m.projects[1]
assert p1.path == 'project-path'
assert Path(p1.abspath) == topdir / 'project-path'
assert p1.posixpath == (topdir / 'project-path').as_posix()
def test_manifest_path_not_found(tmp_workspace):
# Make sure manifest_path() raises FileNotFoundError if the
# manifest file specified in .west/config doesn't exist.
# Here, we rely on tmp_workspace not actually creating the file.
with pytest.raises(FileNotFoundError) as e:
manifest_path()
assert e.value.filename == tmp_workspace / 'mp' / 'west.yml'
def test_manifest_path_conflicts(tmp_workspace):
# Project path conflicts with the manifest path are errors. This
# is true when we have an explicit file system path, but it is not
# true when loading from data, where absolute paths are not known
# and the actual location of the manifest may be overridden from
# "self: path:", e.g. with "west init -l".
with open(tmp_workspace / 'mp' / 'west.yml', 'w', encoding='utf-8') as f:
f.write('''
manifest:
projects:
- name: p
path: mp
url: u
''')
with pytest.raises(MalformedManifest) as e:
Manifest.from_topdir(topdir=tmp_workspace)
assert 'p path "mp" is taken by the manifest repository' in str(e.value)
m = M('''\
projects:
- name: n
url: u
path: p
self:
path: p
''')
assert m.yaml_path == 'p'
assert m.abspath is None
assert m.projects[1].path == 'p'
assert m.projects[1].abspath is None
def test_manifest_repo_discovery(manifest_repo):
# The API should be able to find a manifest file based on the file
# system and west configuration. The resulting topdir and abspath
# attributes should work as specified.
topdir = manifest_repo.topdir
with open(manifest_repo / 'west.yml', 'w') as f:
f.write('''\
manifest:
projects:
- name: project-from-manifest-dir
url: from-manifest-dir
''')
# manifest_path() should discover west_yml.
assert manifest_path() == manifest_repo / 'west.yml'
# Manifest.from_file() should as well.
# The project hierarchy should be rooted in the topdir.
manifest = Manifest.from_file()
assert manifest.topdir is not None
assert Path(manifest.topdir) == topdir
assert len(manifest.projects) == 2
p = manifest.projects[1]
assert p.name == 'project-from-manifest-dir'
assert p.url == 'from-manifest-dir'
assert p.topdir is not None
assert PurePath(p.topdir) == topdir
# Manifest.from_topdir() should work similarly.
manifest = Manifest.from_topdir()
assert Path(manifest.topdir) == topdir
def test_parse_multiple_manifest_files(manifest_repo):
# The API should be able to parse multiple manifest files inside a
# single topdir. The project hierarchies should always be rooted
# in that same topdir. The results of parsing the two separate
# files are independent of one another.
topdir = Path(manifest_repo.topdir)
manifest_repo = Path(manifest_repo)
west_yml = manifest_repo / 'west.yml'
with open(west_yml, 'w') as f:
f.write('''\
manifest:
projects:
- name: project-1
url: url-1
- name: project-2
url: url-2
''')
another_repo = topdir / 'another-repo'
create_repo(another_repo)
another_yml = another_repo / 'another.yml'
with open(another_yml, 'w') as f:
f.write('''\
manifest:
projects:
- name: another-1
url: another-url-1
- name: another-2
url: another-url-2
path: another/path
''')
another_yml_with_path = another_repo / 'another-with-path.yml'
with open(another_yml_with_path, 'w') as f:
f.write('''\
manifest:
projects:
- name: foo
url: bar
self:
path: yaml-path
''')
# manifest_path() should discover west_yml.
assert Path(manifest_path()) == west_yml
# Manifest.from_file() should discover west.yml, and
# the project hierarchy should be rooted at topdir.
manifest = Manifest.from_file()
assert Path(manifest.topdir) == topdir
assert len(manifest.projects) == 3
assert manifest.projects[1].name == 'project-1'
assert manifest.projects[2].name == 'project-2'
# Manifest.from_file() should be also usable with another_yml.
# The project hierarchy in its return value should still be rooted
# in the topdir, but the resulting manifest will be initialized
# as if from "another_repo".
manifest = Manifest.from_file(source_file=another_yml)
assert len(manifest.projects) == 3
assert manifest.topdir is not None
assert Path(manifest.topdir) == topdir
assert Path(manifest.abspath) == another_yml
assert Path(manifest.repo_abspath) == another_repo
mproj = manifest.projects[0]
assert Path(mproj.path) == Path('another-repo')
assert Path(mproj.abspath) == another_repo
assert mproj.posixpath == another_repo.as_posix()
p1 = manifest.projects[1]
assert p1.name == 'another-1'
assert p1.url == 'another-url-1'
assert Path(p1.topdir) == topdir
assert PurePath(p1.abspath) == topdir / 'another-1'
p2 = manifest.projects[2]
assert p2.name == 'another-2'
assert p2.url == 'another-url-2'
assert Path(p2.topdir) == topdir
assert Path(p2.abspath) == topdir / 'another' / 'path'
# If the manifest yaml file does specify its path, the yaml_path
# attribute should reflect that, but we should still reflect what
# we actually loaded.
manifest = Manifest.from_file(source_file=another_yml_with_path)
assert manifest.yaml_path == 'yaml-path'
assert Path(manifest.abspath) == another_yml_with_path
mproj = manifest.projects[0]
assert Path(mproj.abspath) == another_repo
def test_bad_topdir_fails(tmp_workspace):
# Make sure we get expected failure using Manifest.from_topdir()
# with the topdir kwarg when no west.yml exists.
with pytest.raises(FileNotFoundError):
Manifest.from_topdir(topdir=tmp_workspace)