mirror of https://github.com/Chizi123/Arch-autobuild-repo.git

Joel Grunbaum
yesterday b7cd53c75bf46cf7e7d35e36a415722bdb432b08
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
#!/usr/bin/env python3
"""
Integration test script for archrepobuild.
 
This script creates a temporary repository, initializes it with a basic config,
adds test packages, and verifies they build and are added correctly.
 
Usage:
    python tests/integration_test.py [--keep-temp]
    
Options:
    --keep-temp    Don't delete temporary directory after test (for debugging)
"""
 
import asyncio
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
 
# Add src to path for development
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
 
from archrepobuild.aur import AURClient
from archrepobuild.builder import Builder, BuildStatus
from archrepobuild.config import Config, RepositoryConfig, BuildingConfig, SigningConfig, PackageOverride
from archrepobuild.logging import setup_logging, console
from archrepobuild.repo import RepoManager
 
 
# Test packages - real packages that exist in the AUR
# Chosen for small size and fast build times
TEST_PACKAGES = [
    "neofetch-git",  # Small bash script, very fast build
    "yay",           # Popular AUR helper
]
 
# Alternative packages if the above aren't available
FALLBACK_PACKAGES = [
    "gtk2",          # Legacy GTK library
    "paru",          # Another AUR helper
]
 
 
class IntegrationTest:
    """Integration test runner."""
 
    def __init__(self, keep_temp: bool = False, use_cli: bool = False, use_binary: bool = False):
        self.keep_temp = keep_temp
        self.use_cli = use_cli
        self.use_binary = use_binary
        self.temp_dir: Path | None = None
        self.config: Config | None = None
        self.config_path: Path | None = None
        self.passed = 0
        self.failed = 0
 
    def _run_cli(self, command: str, *args: str) -> subprocess.CompletedProcess:
        """Run archrepobuild CLI command via subprocess."""
        if not self.config_path:
            raise RuntimeError("Config path not set")
 
        env = os.environ.copy()
        
        if self.use_binary:
            binary_path = Path(__file__).parent.parent / "dist" / "archrepobuild"
            if not binary_path.exists():
                raise RuntimeError(f"Binary not found at {binary_path}. Run scripts/build_binary.py first.")
            
            cmd = [str(binary_path), "-c", str(self.config_path), command] + list(args)
            # No PYTHONPATH needed for the standalone binary
        else:
            # Add src to PYTHONPATH so the CLI can find the package
            env["PYTHONPATH"] = str(Path(__file__).parent.parent / "src")
            cmd = [sys.executable, "-m", "archrepobuild.cli", "-c", str(self.config_path), command] + list(args)
            
        return subprocess.run(cmd, capture_output=True, text=True, env=env)
 
    def setup(self) -> None:
        """Set up temporary test environment."""
        console.print("\n[bold blue]═══ Setting up test environment ═══[/]")
        
        # Create temp directory
        self.temp_dir = Path(tempfile.mkdtemp(prefix="archrepobuild_test_"))
        console.print(f"  Created temp directory: {self.temp_dir}")
 
        # Create subdirectories
        repo_dir = self.temp_dir / "repo"
        build_dir = self.temp_dir / "build"
        repo_dir.mkdir()
        build_dir.mkdir()
 
        # Create custom makepkg.conf that disables debug packages 
        # (avoids needing debugedit which may not be installed)
        makepkg_conf = self.temp_dir / "makepkg.conf"
        makepkg_conf.write_text("""
# Minimal makepkg.conf for testing
CARCH="x86_64"
CHOST="x86_64-pc-linux-gnu"
CFLAGS="-O2 -pipe"
CXXFLAGS="$CFLAGS"
LDFLAGS=""
MAKEFLAGS="-j$(nproc)"
OPTIONS=(!debug !strip !staticlibs)
PKGEXT='.pkg.tar.zst'
SRCEXT='.src.tar.gz'
PACKAGER="Integration Test <test@test.local>"
 
DLAGENTS=('file::/usr/bin/curl -qgC - -o %o %u'
          'ftp::/usr/bin/curl -qgfC - --ftp-pasv --retry 3 --retry-delay 3 -o %o %u'
          'http::/usr/bin/curl -qgb "" -fLC - --retry 3 --retry-delay 3 -o %o %u'
          'https::/usr/bin/curl -qgb "" -fLC - --retry 3 --retry-delay 3 -o %o %u'
          'rsync::/usr/bin/rsync --no-motd -z %u %o'
          'scp::/usr/bin/scp -C %u %o')
 
VCSCLIENTS=('git::git'
            'hg::mercurial'
            'svn::subversion')
""")
 
        self.config = Config(
            repository=RepositoryConfig(
                name="testrepo",
                path=repo_dir,
                build_dir=build_dir,
                compression="zst",
            ),
            building=BuildingConfig(
                parallel=False,  # Sequential for cleaner test output
                max_workers=1,
                clean=True,
                update_system=False,
                retry_attempts=2,
            ),
            signing=SigningConfig(
                enabled=False,
            ),
            log_level="INFO",
            # Apply package overrides to use our custom makepkg.conf
            package_overrides={
                "_default": PackageOverride(
                    extra_args=["--config", str(makepkg_conf)],
                ),
            },
        )
 
        # Save config to disk for CLI to use
        from archrepobuild.config import save_config
        self.config_path = self.temp_dir / "config.yaml"
        save_config(self.config, self.config_path)
 
        setup_logging(self.config.log_level)
        console.print("  [green]✓[/] Configuration created")
 
    def teardown(self) -> None:
        """Clean up test environment."""
        if self.temp_dir and self.temp_dir.exists():
            if self.keep_temp:
                console.print(f"\n[yellow]Keeping temp directory:[/] {self.temp_dir}")
            else:
                shutil.rmtree(self.temp_dir)
                console.print("\n[dim]Cleaned up temp directory[/]")
 
    def check(self, condition: bool, message: str) -> bool:
        """Check a condition and report pass/fail."""
        if condition:
            console.print(f"  [green]✓[/] {message}")
            self.passed += 1
            return True
        else:
            console.print(f"  [red]✗[/] {message}")
            self.failed += 1
            return False
 
    async def test_init(self) -> bool:
        """Test repository initialization."""
        console.print("\n[bold blue]═══ Test: Repository Initialization ═══[/]")
 
        if self.use_cli:
            result = self._run_cli("init")
            self.check(result.returncode == 0, "CLI: init command success")
        else:
            repo = RepoManager(self.config)
            repo.ensure_repo_exists()
 
        # Check directories exist
        self.check(
            self.config.repository.path.exists(),
            f"Repository directory exists: {self.config.repository.path}"
        )
        self.check(
            self.config.repository.build_dir.exists(),
            f"Build directory exists: {self.config.repository.build_dir}"
        )
 
        return self.failed == 0
 
    async def test_aur_client(self) -> list[str]:
        """Test AUR client and find available test packages."""
        console.print("\n[bold blue]═══ Test: AUR Client ═══[/]")
 
        available_packages = []
 
        async with AURClient() as aur:
            # Test package lookup
            for pkg_name in TEST_PACKAGES + FALLBACK_PACKAGES:
                pkg = await aur.get_package(pkg_name)
                if pkg:
                    self.check(True, f"Found package: {pkg_name} ({pkg.version})")
                    available_packages.append(pkg_name)
                    if len(available_packages) >= 2:
                        break
                else:
                    console.print(f"  [yellow]⚠[/] Package not found: {pkg_name}")
 
        self.check(
            len(available_packages) >= 1,
            f"Found {len(available_packages)} test package(s)"
        )
 
        return available_packages
 
    async def test_build_packages(self, packages: list[str]) -> dict[str, BuildStatus]:
        """Test building packages."""
        console.print("\n[bold blue]═══ Test: Package Building ═══[/]")
 
        results: dict[str, BuildStatus] = {}
 
        if self.use_cli:
            for pkg_name in packages:
                console.print(f"\n  Building {pkg_name} via CLI...")
                result = self._run_cli("build", pkg_name, "-f")
                
                if result.returncode == 0:
                    results[pkg_name] = BuildStatus.SUCCESS
                    self.check(True, f"CLI: Built {pkg_name} successfully")
                    
                    # Check for created artifacts
                    pkg_dir = self.config.repository.build_dir / pkg_name
                    artifacts = list(pkg_dir.glob("*.pkg.tar.*"))
                    artifacts = [a for a in artifacts if not a.name.endswith(".sig")]
                    self.check(len(artifacts) > 0, f"  Created {len(artifacts)} artifact(s)")
                else:
                    results[pkg_name] = BuildStatus.FAILED
                    self.check(False, f"CLI: Failed to build {pkg_name}\nError: {result.stderr}")
        else:
            async with AURClient() as aur:
                async with Builder(self.config, aur) as builder:
                    for pkg_name in packages:
                        console.print(f"\n  Building {pkg_name}...")
                        result = await builder.build_package(pkg_name, force=True)
                        results[pkg_name] = result.status
 
                        if result.status == BuildStatus.SUCCESS:
                            self.check(True, f"Built {pkg_name} successfully ({result.duration:.1f}s)")
                            self.check(
                                len(result.artifacts) > 0,
                                f"  Created {len(result.artifacts)} artifact(s)"
                            )
                            for artifact in result.artifacts:
                                console.print(f"    → {artifact.name}")
                        else:
                            self.check(False, f"Failed to build {pkg_name}: {result.error}")
 
        return results
 
    async def test_repo_add(self, packages: list[str]) -> None:
        """Test adding packages to repository."""
        console.print("\n[bold blue]═══ Test: Repository Management ═══[/]")
 
        if self.use_cli:
            # In CLI mode, 'add' or 'build' already adds to repo, but we can test 'remake'
            result = self._run_cli("remake")
            self.check(result.returncode == 0, "CLI: remake command success")
        else:
            repo = RepoManager(self.config)
            async with AURClient() as aur:
                async with Builder(self.config, aur) as builder:
                    for pkg_name in packages:
                        # Get the build result with artifacts
                        pkg_dir = self.config.repository.build_dir / pkg_name
                        if not pkg_dir.exists():
                            continue
 
                        # Find artifacts
                        artifacts = list(pkg_dir.glob("*.pkg.tar.*"))
                        artifacts = [a for a in artifacts if not a.name.endswith(".sig")]
 
                        if artifacts:
                            # Create a mock build result for add_packages
                            from archrepobuild.builder import BuildResult
                            mock_result = BuildResult(
                                package=pkg_name,
                                status=BuildStatus.SUCCESS,
                                artifacts=artifacts,
                            )
                            success = repo.add_packages(mock_result)
                            self.check(success, f"Added {pkg_name} to repository")
 
        # List packages in repo
        repo = RepoManager(self.config)
        pkg_list = repo.list_packages()
        self.check(len(pkg_list) > 0, f"Repository contains {len(pkg_list)} package(s)")
 
        for pkg in pkg_list:
            console.print(f"    → {pkg.name} {pkg.version} ({pkg.size / 1024:.1f} KB)")
 
    async def test_repo_database(self) -> None:
        """Test repository database integrity."""
        console.print("\n[bold blue]═══ Test: Database Integrity ═══[/]")
 
        if self.use_cli:
            # We already tested remake, let's just check the DB file
            pass
 
        db_path = self.config.repository.path / f"{self.config.repository.name}.db.tar.zst"
        
        self.check(db_path.exists(), f"Database file exists: {db_path.name}")
 
        if db_path.exists():
            # Try to list contents with tar
            result = subprocess.run(
                ["tar", "-tf", str(db_path)],
                capture_output=True,
                text=True,
            )
            if result.returncode == 0:
                entries = [e for e in result.stdout.strip().split("\n") if e]
                self.check(
                    len(entries) > 0,
                    f"Database contains {len(entries)} entries"
                )
 
        # Check for integrity issues
        repo = RepoManager(self.config)
        issues = repo.check_integrity()
        self.check(
            len(issues) == 0,
            f"No integrity issues found" if not issues else f"Issues: {issues}"
        )
 
    async def test_cleanup(self) -> None:
        """Test package cleanup functionality."""
        console.print("\n[bold blue]═══ Test: Cleanup ═══[/]")
 
        if self.use_cli:
            result = self._run_cli("cleanup")
            self.check(result.returncode == 0, "CLI: cleanup command success")
        else:
            repo = RepoManager(self.config)
            removed = repo.cleanup()
            self.check(True, f"Cleanup removed {removed} old version(s)")
 
    async def run(self) -> bool:
        """Run all integration tests."""
        mode_str = " (Python Mode)"
        if self.use_binary:
            mode_str = " (Binary Mode)"
        elif self.use_cli:
            mode_str = " (CLI Mode)"
            
        console.print("[bold magenta]╔════════════════════════════════════════════╗[/]")
        console.print(f"[bold magenta]║     Archbuild Integration Test Suite{mode_str:<10}║[/]")
        console.print("[bold magenta]╚════════════════════════════════════════════╝[/]")
 
        try:
            self.setup()
 
            # Run tests
            await self.test_init()
 
            packages = await self.test_aur_client()
            if not packages:
                console.print("[red]No test packages available, cannot continue[/]")
                return False
 
            build_results = await self.test_build_packages(packages)  # Build all found packages
            
            successful = [p for p, s in build_results.items() if s == BuildStatus.SUCCESS]
            if successful:
                await self.test_repo_add(successful)
                await self.test_repo_database()
                await self.test_cleanup()
            else:
                console.print("[yellow]No successful builds to test repository with[/]")
 
            # Summary
            console.print("\n[bold blue]═══ Test Summary ═══[/]")
            total = self.passed + self.failed
            console.print(f"  Total:  {total}")
            console.print(f"  [green]Passed: {self.passed}[/]")
            console.print(f"  [red]Failed: {self.failed}[/]")
 
            if self.failed == 0:
                console.print("\n[bold green]✓ All tests passed![/]")
                return True
            else:
                console.print(f"\n[bold red]✗ {self.failed} test(s) failed[/]")
                return False
 
        except KeyboardInterrupt:
            console.print("\n[yellow]Test interrupted[/]")
            return False
        except Exception as e:
            console.print(f"\n[bold red]Test error:[/] {e}")
            import traceback
            traceback.print_exc()
            return False
        finally:
            self.teardown()
 
 
async def main() -> int:
    """Main entry point."""
    keep_temp = "--keep-temp" in sys.argv
    use_cli = "--use-cli" in sys.argv
    use_binary = "--use-binary" in sys.argv
 
    if use_binary:
        use_cli = True  # Binary mode is a sub-mode of CLI mode
 
    # Check if running on Arch Linux
    if not Path("/etc/arch-release").exists():
        console.print("[yellow]Warning: Not running on Arch Linux[/]")
        console.print("[yellow]Some tests may fail or be skipped[/]")
 
    # Check for required tools
    for tool in ["makepkg", "pacman", "git"]:
        result = subprocess.run(["which", tool], capture_output=True)
        if result.returncode != 0:
            console.print(f"[red]Required tool not found: {tool}[/]")
            return 1
 
    test = IntegrationTest(keep_temp=keep_temp, use_cli=use_cli, use_binary=use_binary)
    success = await test.run()
 
    return 0 if success else 1
 
 
if __name__ == "__main__":
    exit_code = asyncio.run(main())
    sys.exit(exit_code)