• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pact-foundation / pact-go / 18960674351

31 Oct 2025 02:18AM UTC coverage: 30.707% (+2.4%) from 28.272%
18960674351

Pull #455

github

YOU54F
feat: pact-go test wrapper around go test

to automatically set PACT_LD_LIBRARY_PATH
Pull Request #455: Feat/purego

384 of 512 new or added lines in 8 files covered. (75.0%)

55 existing lines in 2 files now uncovered.

2003 of 6523 relevant lines covered (30.71%)

16.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.31
/installer/installer.go
1
// Package installer is responsible for finding, acquiring and addressing
2
// runtime dependencies for this package (e.g. Ruby standalone, Rust bindings etc.)
3
package installer
4

5
import (
6
        "compress/gzip"
7
        "fmt"
8
        "io"
9
        "log"
10
        "net/http"
11
        "os"
12
        "os/exec"
13
        "os/user"
14
        "path"
15
        "path/filepath"
16
        "runtime"
17
        "strings"
18

19
        goversion "github.com/hashicorp/go-version"
20
        "gopkg.in/yaml.v3"
21

22
        "crypto/md5"
23

24
        "github.com/spf13/afero"
25
)
26

27
// NativeLibPath returns the absolute path to the go package used to link to the native rust library
28
func NativeLibPath() string {
12✔
29
        _, file, _, ok := runtime.Caller(0)
12✔
30
        if !ok {
12✔
31
                return ""
×
32
        }
×
33
        pactRoot := filepath.Dir(filepath.Dir(file))
12✔
34
        return filepath.Join(pactRoot, "internal", "native")
12✔
35
}
36

37
// Installer is used to check the Pact Go installation is setup correctly, and can automatically install
38
// packages if required
39
type Installer struct {
40
        downloader downloader
41
        hasher     hasher
42
        config     configReadWriter
43
        os         string
44
        arch       string
45
        fs         afero.Fs
46
        libDir     string
47
        force      bool
48
}
49

50
type installerConfig func(*Installer) error
51

52
// NewInstaller creates a new initialised Installer
53
func NewInstaller(opts ...installerConfig) (*Installer, error) {
12✔
54
        i := &Installer{downloader: &defaultDownloader{}, fs: afero.NewOsFs(), hasher: &defaultHasher{}, config: &configuration{}}
12✔
55

12✔
56
        for _, opt := range opts {
24✔
57
                err := opt(i)
12✔
58
                if err != nil {
12✔
59
                        log.Println("[ERROR] failure when configuring installer:", err)
×
60
                        return nil, err
×
61
                }
×
62
        }
63

64
        if _, ok := supportedOSes[runtime.GOOS]; !ok {
12✔
65
                return nil, fmt.Errorf("%s is not a supported OS", runtime.GOOS)
×
66
        }
×
67
        i.os = supportedOSes[runtime.GOOS]
12✔
68

12✔
69
        if !strings.Contains(runtime.GOARCH, "64") {
12✔
70
                return nil, fmt.Errorf("%s is not a supported architecture, only 64 bit architectures are supported", runtime.GOARCH)
×
71
        }
×
72

73
        switch runtime.GOARCH {
12✔
74
        case "amd64":
8✔
75
                i.arch = x86_64
8✔
76
        case "arm64":
4✔
77
                i.arch = aarch64
4✔
78
        default:
×
79
                i.arch = x86_64
×
80
                log.Println("[WARN] amd64 architecture not detected, defaulting to x86_64. Behaviour may be undefined")
×
81
        }
82

83
        return i, nil
12✔
84
}
85

86
// SetLibDir overrides the default library dir
87
func (i *Installer) SetLibDir(dir string) {
×
88
        i.libDir = dir
×
89
}
×
90

91
// SetConfigLibDir stores the libDir in the config
NEW
92
func (i *Installer) SetConfigLibDir(dir string) {
×
NEW
93
        // print debug log
×
NEW
94
        log.Printf("[DEBUG] setting config lib dir to %s", dir)
×
NEW
95
        c := i.config.readConfig()
×
NEW
96
        c.LibDir = dir
×
NEW
97
        i.config.writeConfig(c)
×
NEW
98
}
×
99

100
// GetConfigLibDir retrieves the libDir from config
NEW
101
func GetConfigLibDir() string {
×
NEW
102
        c := &configuration{}
×
NEW
103
        config := c.readConfig()
×
NEW
104
        return config.LibDir
×
NEW
105
}
×
106

107
// GetActualLibDir returns the actual library directory being used
NEW
108
func (i *Installer) GetActualLibDir() string {
×
NEW
109
        return i.getLibDir()
×
NEW
110
}
×
111

112
// Force installs over the top
NEW
113
func (i *Installer) Force(force bool) {
×
114
        i.force = force
×
115
}
×
116

117
// CheckInstallation checks installation of all of the required libraries
118
// and downloads if they aren't present
119
func (i *Installer) CheckInstallation() error {
36✔
120

36✔
121
        // Check if files exist
36✔
122
        // --> Check if existing installed files
36✔
123
        if !i.force {
72✔
124
                if err := i.CheckPackageInstall(); err == nil {
48✔
125
                        return nil
12✔
126
                }
12✔
127
        }
128

129
        // Download dependencies
130
        if err := i.downloadDependencies(); err != nil {
24✔
UNCOV
131
                return err
×
UNCOV
132
        }
×
133

134
        // Install dependencies
135
        if err := i.installDependencies(); err != nil {
28✔
136
                return err
4✔
137
        }
4✔
138

139
        // Double check files landed correctly (can't execute 'version' call here,
140
        // because of dependency on the native libs we're trying to download!)
141
        if err := i.CheckPackageInstall(); err != nil {
28✔
142
                return fmt.Errorf("unable to verify downloaded/installed dependencies: %s", err)
8✔
143
        }
8✔
144

145
        return nil
12✔
146
}
147

148
func (i *Installer) getLibDir() string {
140✔
149
        if i.libDir != "" {
140✔
UNCOV
150
                return i.libDir
×
UNCOV
151
        }
×
152

153
        env := os.Getenv(downloadEnvVar)
140✔
154
        if env != "" {
140✔
UNCOV
155
                return env
×
UNCOV
156
        }
×
157
        homeDir, err := os.UserHomeDir()
140✔
158
        if err != nil {
140✔
NEW
159
                log.Printf("[WARN] Could not determine home directory: %v", err)
×
NEW
160
                return ""
×
NEW
161
        }
×
162

163
        return filepath.Join(homeDir, ".pact", "pact-go", "libs")
140✔
164
}
165

166
// CheckPackageInstall discovers any existing packages, and checks installation of a given binary using semver-compatible checks
167
func (i *Installer) CheckPackageInstall() error {
56✔
168
        for pkg, info := range packages {
112✔
169

56✔
170
                dst, _ := i.getLibDstForPackage(pkg)
56✔
171

56✔
172
                if _, err := i.fs.Stat(dst); err != nil {
88✔
173
                        log.Println("[INFO] package", info.libName, "not found")
32✔
174
                        return err
32✔
175
                } else {
56✔
176
                        log.Println("[INFO] package", info.libName, "found")
24✔
177
                }
24✔
178

179
                lib, ok := i.config.readConfig().Libraries[pkg]
24✔
180
                if ok {
24✔
UNCOV
181
                        if err := checkVersion(info.libName, lib.Version, info.semverRange); err != nil {
×
UNCOV
182
                                return err
×
183
                        }
×
184
                        log.Println("[INFO] package", info.libName, "is correctly installed")
×
185
                } else {
24✔
186
                        log.Println("[INFO] no package metadata information was found, run `pact-go install -f` to correct")
24✔
187
                }
24✔
188

189
                // This will only be populated during test when the ffi is loaded, but will actually test the FFI itself
190
                // It is helpful because it will prevent issues where the FFI is manually updated without using the `pact-go install` command
191
                if len(LibRegistry) == 0 {
48✔
192
                        log.Println("[DEBUG] skip checking ffi version() call because FFI not loaded. This is expected when running the 'pact-go' command.")
24✔
193
                } else {
24✔
UNCOV
194
                        lib, ok := LibRegistry[pkg]
×
UNCOV
195

×
196
                        if ok {
×
197
                                log.Println("[INFO] checking version", lib.Version(), "for lib", info.libName, "within semver range", info.semverRange)
×
198
                                if err := checkVersion(info.libName, lib.Version(), info.semverRange); err != nil {
×
199
                                        return err
×
200
                                }
×
201
                        } else {
×
202
                                log.Println("[DEBUG] unable to determine current version of package", pkg, "in LibRegistry", LibRegistry)
×
203
                        }
×
204

205
                        // Correct the configuration to reduce drift
UNCOV
206
                        err := i.updateConfiguration(dst, pkg, info)
×
UNCOV
207
                        if err != nil {
×
208
                                return err
×
209
                        }
×
210
                }
211
        }
212

213
        return nil
24✔
214
}
215

216
// Download all dependencies, and update the pact-go configuration file
217
func (i *Installer) downloadDependencies() error {
36✔
218
        for pkg, pkgInfo := range packages {
72✔
219
                src, err := i.getDownloadURLForPackage(pkg)
36✔
220

36✔
221
                if err != nil {
36✔
UNCOV
222
                        return err
×
UNCOV
223
                }
×
224

225
                dst, err := i.getLibDstForPackage(pkg)
36✔
226

36✔
227
                if err != nil {
36✔
UNCOV
228
                        return err
×
UNCOV
229
                }
×
230

231
                err = i.downloader.download(src, dst)
36✔
232

36✔
233
                if err != nil {
36✔
UNCOV
234
                        return err
×
UNCOV
235
                }
×
236

237
                err = os.Chmod(dst, 0755)
36✔
238
                if err != nil {
44✔
239
                        log.Println("[WARN] unable to set permissions on file", dst, "due to error:", err)
8✔
240
                }
8✔
241

242
                err = i.updateConfiguration(dst, pkg, pkgInfo)
36✔
243

36✔
244
                if err != nil {
36✔
UNCOV
245
                        return err
×
UNCOV
246
                }
×
247
        }
248

249
        return nil
36✔
250
}
251

252
func (i *Installer) installDependencies() error {
24✔
253
        if i.os == macos {
48✔
254
                for pkg, info := range packages {
48✔
255
                        log.Println("[INFO] setting install_name on library", info.libName, "for macos")
24✔
256

24✔
257
                        dst, err := i.getLibDstForPackage(pkg)
24✔
258

24✔
259
                        if err != nil {
24✔
UNCOV
260
                                return err
×
UNCOV
261
                        }
×
262

263
                        err = setMacOSInstallName(dst)
24✔
264

24✔
265
                        if err != nil {
28✔
266
                                return err
4✔
267
                        }
4✔
268
                }
269
        }
270

271
        return nil
20✔
272
}
273

274
// returns src
275
func (i *Installer) getDownloadURLForPackage(pkg string) (string, error) {
96✔
276
        pkgInfo, ok := packages[pkg]
96✔
277
        if !ok {
96✔
UNCOV
278
                return "", fmt.Errorf("unable to find package details for package: %s", pkg)
×
UNCOV
279
        }
×
280

281
        if checkMusl() && i.os == linux {
96✔
UNCOV
282
                return fmt.Sprintf(downloadTemplate, pkg, pkgInfo.version, osToLibName[i.os], i.os, i.arch+"-musl", osToExtension[i.os]), nil
×
283
        } else {
96✔
284
                return fmt.Sprintf(downloadTemplate, pkg, pkgInfo.version, osToLibName[i.os], i.os, i.arch, osToExtension[i.os]), nil
96✔
285

96✔
286
        }
96✔
287

288
}
289

290
func (i *Installer) getLibDstForPackage(pkg string) (string, error) {
140✔
291
        _, ok := packages[pkg]
140✔
292
        if !ok {
140✔
UNCOV
293
                return "", fmt.Errorf("unable to find package details for package: %s", pkg)
×
UNCOV
294
        }
×
295

296
        return path.Join(i.getLibDir(), osToLibName[i.os]) + "." + osToExtension[i.os], nil
140✔
297
}
298

299
// Write the metadata to reduce drift
300
func (i *Installer) updateConfiguration(dst string, pkg string, info packageInfo) error {
36✔
301
        // Get hash of file
36✔
302
        fmt.Println(i.hasher)
36✔
303
        hash, err := i.hasher.hash(dst)
36✔
304
        if err != nil {
36✔
UNCOV
305
                return err
×
UNCOV
306
        }
×
307

308
        // Read metadata
309
        c := i.config.readConfig()
36✔
310

36✔
311
        // Update config
36✔
312
        c.Libraries[pkg] = packageMetadata{
36✔
313
                LibName: info.libName,
36✔
314
                Version: info.version,
36✔
315
                Hash:    hash,
36✔
316
        }
36✔
317

36✔
318
        // Write metadata
36✔
319
        return i.config.writeConfig(c)
36✔
320
}
321

322
var setMacOSInstallName = func(file string) error {
12✔
323
        cmd := exec.Command("install_name_tool", "-id", file, file)
12✔
324
        log.Println("[DEBUG] running command:", cmd)
12✔
325
        stdoutStderr, err := cmd.CombinedOutput()
12✔
326

12✔
327
        if err != nil {
16✔
328
                return fmt.Errorf("error setting install name on pact lib: %s", err)
4✔
329
        }
4✔
330

331
        log.Println("[DEBUG] output from command", stdoutStderr)
8✔
332

8✔
333
        return err
8✔
334
}
335

UNCOV
336
func checkVersion(lib, version, versionRange string) error {
×
UNCOV
337
        log.Println("[INFO] checking version", version, "of", lib, "against semver constraint", versionRange)
×
338

×
339
        v, err := goversion.NewVersion(version)
×
340
        if err != nil {
×
341
                return err
×
342
        }
×
343

344
        constraints, err := goversion.NewConstraint(versionRange)
×
UNCOV
345
        if err != nil {
×
346
                return err
×
347
        }
×
348

349
        if constraints.Check(v) {
×
UNCOV
350
                log.Println("[DEBUG]", v, "satisfies constraints", v, constraints)
×
351
                return nil
×
352
        }
×
353

354
        return fmt.Errorf("version %s of %s does not match constraint %s", version, lib, versionRange)
×
355
}
356

357
// checkMusl checks if the OS uses musl library instead of glibc
358
func checkMusl() bool {
120✔
359
        lddPath, err := exec.LookPath("ldd")
120✔
360
        if err != nil {
200✔
361
                return false
80✔
362
        }
80✔
363

364
        cmd := exec.Command(lddPath, "/bin/echo")
40✔
365
        out, err := cmd.CombinedOutput()
40✔
366

40✔
367
        if err != nil {
40✔
UNCOV
368
                return false
×
UNCOV
369
        }
×
370
        if strings.Contains(string(out), "musl") {
40✔
371
                return true
×
UNCOV
372
        }
×
373

374
        return false
40✔
375
}
376

377
// download template structure: "https://github.com/pact-foundation/pact-reference/releases/download/PACKAGE-vVERSION/LIBNAME-OS-ARCH.EXTENSION.gz"
378
var downloadTemplate = "https://github.com/pact-foundation/pact-reference/releases/download/%s-v%s/%s-%s-%s.%s.gz"
379

380
var supportedOSes = map[string]string{
381
        "darwin": macos,
382
        windows:  windows,
383
        linux:    linux,
384
}
385

386
var osToExtension = map[string]string{
387
        windows: "dll",
388
        linux:   "so",
389
        macos:   "dylib",
390
}
391

392
var osToLibName = map[string]string{
393
        windows: "pact_ffi",
394
        linux:   "libpact_ffi",
395
        macos:   "libpact_ffi",
396
}
397

398
type packageInfo struct {
399
        libName     string
400
        version     string
401
        semverRange string
402
}
403

404
const (
405
        FFIPackage     = "libpact_ffi"
406
        downloadEnvVar = "PACT_GO_LIB_DOWNLOAD_PATH"
407
        linux          = "linux"
408
        windows        = "windows"
409
        macos          = "macos"
410
        x86_64         = "x86_64"
411
        aarch64        = "aarch64"
412
)
413

414
var packages = map[string]packageInfo{
415
        FFIPackage: {
416
                libName:     "libpact_ffi",
417
                version:     "0.4.28",
418
                semverRange: ">= 0.4.0, < 1.0.0",
419
        },
420
}
421

422
type Versioner interface {
423
        Version() string
424
}
425

426
var LibRegistry = map[string]Versioner{}
427

428
type downloader interface {
429
        download(src string, dst string) error
430
}
431

432
type defaultDownloader struct{}
433

UNCOV
434
func (d *defaultDownloader) download(src string, dst string) error {
×
UNCOV
435
        log.Println("[INFO] downloading library from", src, "to", dst)
×
436

×
437
        baseDir := path.Dir(dst)
×
438
        if err := os.MkdirAll(baseDir, 0755); err != nil {
×
439
                return fmt.Errorf("failed to create %s; %w", baseDir, err)
×
440
        }
×
441

442
        f, err := os.Create(dst)
×
UNCOV
443
        if err != nil {
×
444
                return fmt.Errorf("failed to create output file; %w", err)
×
445
        }
×
446
        defer f.Close()
×
447

×
448
        resp, err := http.Get(src)
×
449
        if err != nil {
×
450
                return fmt.Errorf("failed http call to %s; %w", src, err)
×
451
        }
×
452
        defer resp.Body.Close()
×
453

×
454
        archive, err := gzip.NewReader(resp.Body)
×
455
        if err != nil {
×
456
                return fmt.Errorf("failed to create new gzip reader; %w", err)
×
457
        }
×
458

459
        _, err = io.Copy(f, archive)
×
UNCOV
460
        if err != nil {
×
461
                return fmt.Errorf("failed to copy archive to file; %w", err)
×
462
        }
×
463

464
        return nil
×
465
}
466

467
type packageMetadata struct {
468
        LibName string
469
        Version string
470
        Hash    string
471
}
472

473
type pactConfig struct {
474
        Libraries map[string]packageMetadata
475
        LibDir    string `yaml:"libDir,omitempty"`
476
}
477

478
type configReader interface {
479
        readConfig() pactConfig
480
}
481
type configWriter interface {
482
        writeConfig(pactConfig) error
483
}
484

485
type configReadWriter interface {
486
        configReader
487
        configWriter
488
}
489

490
type configuration struct{}
491

492
func getConfigPath() string {
24✔
493
        user, err := user.Current()
24✔
494
        if err != nil {
24✔
UNCOV
495
                log.Fatalf("%v", err)
×
UNCOV
496
        }
×
497

498
        return path.Join(user.HomeDir, ".pact", "pact-go.yml")
24✔
499
}
500

501
func (configuration) readConfig() pactConfig {
12✔
502
        pactConfigPath := getConfigPath()
12✔
503
        c := pactConfig{
12✔
504
                Libraries: map[string]packageMetadata{},
12✔
505
        }
12✔
506

12✔
507
        bytes, err := os.ReadFile(pactConfigPath)
12✔
508
        if err != nil {
12✔
UNCOV
509
                log.Println("[DEBUG] error reading file", pactConfigPath, "error: ", err)
×
UNCOV
510
                return c
×
511
        }
×
512

513
        err = yaml.Unmarshal(bytes, &c)
12✔
514
        if err != nil {
12✔
UNCOV
515
                log.Println("[DEBUG] error unmarshalling YAML", pactConfigPath, "error: ", err)
×
UNCOV
516
        }
×
517
        return c
12✔
518
}
519

520
func (configuration) writeConfig(c pactConfig) error {
12✔
521
        log.Println("[DEBUG] writing config", c)
12✔
522
        pactConfigPath := getConfigPath()
12✔
523

12✔
524
        err := os.MkdirAll(filepath.Dir(pactConfigPath), 0755)
12✔
525
        if err != nil {
12✔
UNCOV
526
                log.Println("[DEBUG] error creating pact config directory")
×
UNCOV
527
                return err
×
528
        }
×
529

530
        bytes, err := yaml.Marshal(c)
12✔
531
        if err != nil {
12✔
UNCOV
532
                log.Println("[DEBUG] error marshalling YAML", pactConfigPath, "error: ", err)
×
UNCOV
533
                return err
×
534
        }
×
535
        log.Println("[DEBUG] writing yaml config to file", string(bytes))
12✔
536

12✔
537
        return os.WriteFile(pactConfigPath, bytes, 0644)
12✔
538
}
539

540
type hasher interface {
541
        hash(src string) (string, error)
542
}
543

544
type defaultHasher struct{}
545

546
func (d *defaultHasher) hash(src string) (string, error) {
12✔
547
        log.Println("[DEBUG] obtaining hash for file", src)
12✔
548

12✔
549
        f, err := os.Open(src)
12✔
550
        if err != nil {
12✔
UNCOV
551
                return "", err
×
UNCOV
552
        }
×
553
        defer f.Close()
12✔
554

12✔
555
        h := md5.New()
12✔
556
        if _, err := io.Copy(h, f); err != nil {
12✔
UNCOV
557
                return "", err
×
UNCOV
558
        }
×
559

560
        return fmt.Sprintf("%x", h.Sum(nil)), nil
12✔
561
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc