1# SPDX-License-Identifier: GPL-2.0+
2#
3# Copyright 2025 Simon Glass <sjg@chromium.org>
4#
5"""Functional tests for checking that patman behaves correctly"""
6
7import os
8import shutil
9import tempfile
10
11import pygit2
12
13from u_boot_pylib import gitutil
14from u_boot_pylib import terminal
15from u_boot_pylib import tools
16from u_boot_pylib import tout
17
18
19class TestCommon:
20    """Contains common test functions"""
21    leb = (b'Lord Edmund Blackadd\xc3\xabr <weasel@blackadder.org>'.
22           decode('utf-8'))
23
24    # Fake patchwork project ID for U-Boot
25    PROJ_ID = 6
26    PROJ_LINK_NAME = 'uboot'
27    SERIES_ID_FIRST_V3 = 31
28    SERIES_ID_SECOND_V1 = 456
29    SERIES_ID_SECOND_V2 = 457
30    TITLE_SECOND = 'Series for my board'
31
32    verbosity = False
33    preserve_outdirs = False
34
35    @classmethod
36    def setup_test_args(cls, preserve_indir=False, preserve_outdirs=False,
37                        toolpath=None, verbosity=None, no_capture=False):
38        """Accept arguments controlling test execution
39
40        Args:
41            preserve_indir (bool): not used by patman
42            preserve_outdirs (bool): Preserve the output directories used by
43                tests. Each test has its own, so this is normally only useful
44                when running a single test.
45            toolpath (str): not used by patman
46            verbosity (int): verbosity to use (0 means tout.INIT, 1 means means
47                tout.DEBUG)
48            no_capture (bool): True to output all captured text after capturing
49                completes
50        """
51        del preserve_indir
52        cls.preserve_outdirs = preserve_outdirs
53        cls.toolpath = toolpath
54        cls.verbosity = verbosity
55        cls.no_capture = no_capture
56
57    def __init__(self):
58        super().__init__()
59        self.repo = None
60        self.tmpdir = None
61        self.gitdir = None
62
63    def setUp(self):
64        """Set up the test temporary dir and git dir"""
65        self.tmpdir = tempfile.mkdtemp(prefix='patman.')
66        self.gitdir = os.path.join(self.tmpdir, '.git')
67        tout.init(tout.DEBUG if self.verbosity else tout.INFO,
68                  allow_colour=False)
69
70    def tearDown(self):
71        """Delete the temporary dir"""
72        if self.preserve_outdirs:
73            print(f'Output dir: {self.tmpdir}')
74        else:
75            shutil.rmtree(self.tmpdir)
76        terminal.set_print_test_mode(False)
77
78    def make_commit_with_file(self, subject, body, fname, text):
79        """Create a file and add it to the git repo with a new commit
80
81        Args:
82            subject (str): Subject for the commit
83            body (str): Body text of the commit
84            fname (str): Filename of file to create
85            text (str): Text to put into the file
86        """
87        path = os.path.join(self.tmpdir, fname)
88        tools.write_file(path, text, binary=False)
89        index = self.repo.index
90        index.add(fname)
91        # pylint doesn't seem to find this
92        # pylint: disable=E1101
93        author = pygit2.Signature('Test user', 'test@email.com')
94        committer = author
95        tree = index.write_tree()
96        message = subject + '\n' + body
97        self.repo.create_commit('HEAD', author, committer, message, tree,
98                                [self.repo.head.target])
99
100    def make_git_tree(self):
101        """Make a simple git tree suitable for testing
102
103        It has four branches:
104            'base' has two commits: PCI, main
105            'first' has base as upstream and two more commits: I2C, SPI
106            'second' has base as upstream and three more: video, serial, bootm
107            'third4' has second as upstream and four more: usb, main, test, lib
108
109        Returns:
110            pygit2.Repository: repository
111        """
112        os.environ['GIT_CONFIG_GLOBAL'] = '/dev/null'
113        os.environ['GIT_CONFIG_SYSTEM'] = '/dev/null'
114
115        repo = pygit2.init_repository(self.gitdir)
116        self.repo = repo
117        new_tree = repo.TreeBuilder().write()
118
119        common = ['git', f'--git-dir={self.gitdir}', 'config']
120        tools.run(*(common + ['user.name', 'Dummy']), cwd=self.gitdir)
121        tools.run(*(common + ['user.email', 'dumdum@dummy.com']),
122                  cwd=self.gitdir)
123
124        # pylint doesn't seem to find this
125        # pylint: disable=E1101
126        author = pygit2.Signature('Test user', 'test@email.com')
127        committer = author
128        _ = repo.create_commit('HEAD', author, committer, 'Created master',
129                               new_tree, [])
130
131        self.make_commit_with_file('Initial commit', '''
132Add a README
133
134''', 'README', '''This is the README file
135describing this project
136in very little detail''')
137
138        self.make_commit_with_file('pci: PCI implementation', '''
139Here is a basic PCI implementation
140
141''', 'pci.c', '''This is a file
142it has some contents
143and some more things''')
144        self.make_commit_with_file('main: Main program', '''
145Hello here is the second commit.
146''', 'main.c', '''This is the main file
147there is very little here
148but we can always add more later
149if we want to
150
151Series-to: u-boot
152Series-cc: Barry Crump <bcrump@whataroa.nz>
153''')
154        base_target = repo.revparse_single('HEAD')
155        self.make_commit_with_file('i2c: I2C things', '''
156This has some stuff to do with I2C
157''', 'i2c.c', '''And this is the file contents
158with some I2C-related things in it''')
159        self.make_commit_with_file('spi: SPI fixes', f'''
160SPI needs some fixes
161and here they are
162
163Signed-off-by: {self.leb}
164
165Series-to: u-boot
166Commit-notes:
167title of the series
168This is the cover letter for the series
169with various details
170END
171''', 'spi.c', '''Some fixes for SPI in this
172file to make SPI work
173better than before''')
174        first_target = repo.revparse_single('HEAD')
175
176        target = repo.revparse_single('HEAD~2')
177        # pylint doesn't seem to find this
178        # pylint: disable=E1101
179        repo.reset(target.oid, pygit2.enums.ResetMode.HARD)
180        self.make_commit_with_file('video: Some video improvements', '''
181Fix up the video so that
182it looks more purple. Purple is
183a very nice colour.
184''', 'video.c', '''More purple here
185Purple and purple
186Even more purple
187Could not be any more purple''')
188        self.make_commit_with_file('serial: Add a serial driver', f'''
189Here is the serial driver
190for my chip.
191
192Cover-letter:
193{self.TITLE_SECOND}
194This series implements support
195for my glorious board.
196END
197Series-to: u-boot
198Series-links: {self.SERIES_ID_SECOND_V1}
199''', 'serial.c', '''The code for the
200serial driver is here''')
201        self.make_commit_with_file('bootm: Make it boot', '''
202This makes my board boot
203with a fix to the bootm
204command
205''', 'bootm.c', '''Fix up the bootm
206command to make the code as
207complicated as possible''')
208        second_target = repo.revparse_single('HEAD')
209
210        self.make_commit_with_file('usb: Try out the new DMA feature', '''
211This is just a fix that
212ensures that DMA is enabled
213''', 'usb-uclass.c', '''Here is the USB
214implementation and as you can see it
215it very nice''')
216        self.make_commit_with_file('main: Change to the main program', '''
217Here we adjust the main
218program just a little bit
219''', 'main.c', '''This is the text of the main program''')
220        self.make_commit_with_file('test: Check that everything works', '''
221This checks that all the
222various things we've been
223adding actually work.
224''', 'test.c', '''Here is the test code and it seems OK''')
225        self.make_commit_with_file('lib: Sort out the extra library', '''
226The extra library is currently
227broken. Fix it so that we can
228use it in various place.
229''', 'lib.c', '''Some library code is here
230and a little more''')
231        third_target = repo.revparse_single('HEAD')
232
233        repo.branches.local.create('first', first_target)
234        repo.config.set_multivar('branch.first.remote', '', '.')
235        repo.config.set_multivar('branch.first.merge', '', 'refs/heads/base')
236
237        repo.branches.local.create('second', second_target)
238        repo.config.set_multivar('branch.second.remote', '', '.')
239        repo.config.set_multivar('branch.second.merge', '', 'refs/heads/base')
240
241        repo.branches.local.create('base', base_target)
242
243        repo.branches.local.create('third4', third_target)
244        repo.config.set_multivar('branch.third4.remote', '', '.')
245        repo.config.set_multivar('branch.third4.merge', '',
246                                 'refs/heads/second')
247
248        target = repo.lookup_reference('refs/heads/first')
249        repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE)
250        target = repo.revparse_single('HEAD')
251        repo.reset(target.oid, pygit2.enums.ResetMode.HARD)
252
253        self.assertFalse(gitutil.check_dirty(self.gitdir, self.tmpdir))
254        return repo
255