blob: 0ac078f06d0d5d4f1a1275838e890548f543cf62 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package codifier
import (
"context"
"errors"
"fmt"
"log"
"os/exec"
"strings"
"syscall"
)
// GNFormat calls `gn format` on the stored list of changed files.
func (p *Proc) GNFormat() *Proc {
if p == nil {
log.Println(" GNFormat() error: nil receiver")
return nil
}
if p.changedFiles == nil {
log.Println(" GNFormat(): no changed files to formats")
return p
}
args := append([]string{"format"}, p.changedFiles...)
if _, err := runLoggedCommand(context.Background(), p.fuchsiaDir, "gn", args...); err != nil {
log.Printf(" GNFormat() error: %v", err)
return p
}
log.Println("GNFormat() success")
return p
}
// Build executes GN commands to Build the current Fuchsia repo.
func (p *Proc) Build() *Proc {
if p == nil {
log.Println(" Build() error: nil receiver")
return nil
}
if _, err := runLoggedCommand(context.Background(), p.fuchsiaDir, "fx", "build"); err != nil {
log.Printf(" Build() error: %v", err)
p.buildSuccess = false
return p
}
log.Println("***************************************************************")
log.Println(" Build() success")
log.Println("***************************************************************")
p.buildSuccess = true
return p
}
// Why git integration? Codifier makes changes to gn build files then checks if
// the changes build and pass tests. If successful, the tool continues. A
// complication, however, is determining which changes to undo in the event of a
// build or test failure. The problem is further complicated by subsequent
// changes to previously successfully-changed files. Codifier uses git to ensure
// that after each iteration, we have a successful change or the unsuccessful
// changes are reset but only for that iteration. An additional benefit is that
// when done, the user has a ready-to-upload CL with all of the successful
// changes.
// RestoreOnFail executes Git commands to revert unstaged changes made by
// Codifier.
func (p *Proc) RestoreOnFail() *Proc {
if p == nil {
log.Println(" RestoreOnFail() error: nil receiver")
return nil
}
if p.buildSuccess && p.testSuccess {
log.Println(" RestoreOnFail(): Request to restore, but the the build and test both succeeded")
return p
}
// Changed files:
for _, f := range p.changedFiles {
log.Printf("RestoreOnFail() restoring: %s", f)
if _, err := runLoggedCommand(context.Background(), p.fuchsiaDir, "git", "restore", "--", f); err != nil {
log.Printf(" RestoreOnFail() error: %v", err)
}
}
// Added files:
lines, err := runLoggedCommand(context.Background(), p.fuchsiaDir, "git", "ls-files", "--others", "--exclude-standard")
if err != nil {
log.Printf(" RestoreOnFail() error: %v", err)
}
for _, f := range lines {
f := strings.TrimPrefix(f, "> ") // If added by runner.
log.Printf("RestoreOnFail() removing added file: %q", f)
if strings.Contains(f, "*") {
log.Printf(" RestoreOnFail() error: filename contains wildcard: %q", f)
return nil
}
_, err := runLoggedCommand(context.Background(), p.fuchsiaDir, "rm", f)
if err != nil {
log.Printf(" RestoreOnFail() error: %v", err)
}
}
return p
}
// CommitOrRestore Git commits the changes if the build was successful, else
// does a Git restore to undo changes.
func (p *Proc) CommitOrRestore(commitMsg string, amend bool) *Proc {
if p == nil {
log.Println(" CommitOrRestore() error: nil receiver")
return nil
}
if !p.buildSuccess || !p.testSuccess {
return p.RestoreOnFail()
}
return p.Commit(commitMsg, amend)
}
// Commit does a Git commit, first adding an new files. If amend is true, then
// the prior commit message is used, else a new one.
func (p *Proc) Commit(commitMsg string, amend bool) *Proc {
if p == nil {
log.Println(" Commit() error: nil receiver")
return nil
}
if !p.buildSuccess {
log.Println(" Commit(): Request to commit, but the build failed")
return p
}
if !p.testSuccess {
log.Println(" Commit(): Request to commit, but the test failed")
return p
}
// Add any new files.
if _, err := runLoggedCommand(context.Background(), p.fuchsiaDir, "git", "add", "--", "."); err != nil {
log.Printf(" Commit() error: %v", err)
}
if amend {
if _, err := runLoggedCommand(context.Background(), p.fuchsiaDir, "git", "commit", "-a", "--amend", "--no-edit"); err != nil {
log.Printf(" Commit() error: %v", err)
}
} else {
if _, err := runLoggedCommand(context.Background(), p.fuchsiaDir, "git", "commit", "-a", "-m", commitMsg); err != nil {
log.Printf(" Commit() error: %v", err)
}
}
return p
}
// BuildSucceeded returns true if there has been a successful build.
func (p *Proc) BuildSucceeded() (bool, error) {
if p == nil {
return false, errors.New(" BuildSucceeded() error: nil receiver")
}
return p.buildSuccess, nil
}
// Test runs the Test with the name stored under the given key.
func (p *Proc) Test(key string, chatty bool) *Proc {
if p == nil {
log.Println(" Test() error: nil receiver")
return nil
}
p.testSuccess = false
test, ok := p.Store[key].(string)
if !ok {
log.Printf(" Test() error: test named %q not present in store", key)
return nil
}
if err := newFxTest(p.fuchsiaDir).runTest(test, chatty); err != nil {
log.Printf(" Test(): error running test %q: %v", test, err)
return p
}
log.Println("***************************************************************")
log.Println(" Test() success")
log.Println("***************************************************************")
p.testSuccess = true
return p
}
// TestSucceeded returns true if there has been a successful test.
func (p *Proc) TestSucceeded() (bool, error) {
if p == nil {
return false, errors.New(" TestSucceeded() error: nil receiver")
}
return p.testSuccess, nil
}
// Success returns true if there has been a successful build and test.
func (p *Proc) Success() (bool, error) {
if p == nil {
return false, errors.New(" Success() error: nil receiver")
}
return p.buildSuccess && p.testSuccess, nil
}
// AssertNoBootTest ensures that the test name stored under the given key is
// present in the build output. It is not an error if the key is present but
// the corresponding value is blank. In this case, no check is performed.
func (p *Proc) AssertNoBootTest(key string) *Proc {
if p == nil {
log.Println(" AssertNoBootTest() error: nil receiver")
return nil
}
test, ok := p.Store[key].(string)
if !ok {
log.Printf(" AssertNoBootTest() error: no key %q in store", key)
return nil
}
if test == "" {
log.Printf(" AssertNoBootTest(): test stored under %q is blank", key)
return p // Not an error.
}
log.Printf(" AssertNoBootTest(): looking for test %q", test)
cmd := exec.Command("bash", "-c", "fx test ?")
cmd.Dir = p.fuchsiaDir
out, err := cmd.Output()
// `fx test ?` should return exit code 3 and list the tests for us to search.
if err == nil {
log.Printf(" AssertNoBootTest() error: unexpected exit status of `fx test ?`: 0")
return nil
}
// There was an err, as expected; check the exit code.
if msg, ok := err.(*exec.ExitError); ok { // There is an error code.
exitStatus := msg.Sys().(syscall.WaitStatus).ExitStatus()
if exitStatus != 3 {
log.Printf(" AssertNoBootTest() error: unexpected exit status of `fx test ?`: %d", exitStatus)
return nil
}
}
fullTestPath := fmt.Sprintf("/boot/test/%s", test)
if strings.Index(string(out), fullTestPath) != -1 {
log.Printf(" AssertNoBootTest() error: found %q", fullTestPath)
return nil
}
log.Printf("Confirmed %q not present in bringup", fullTestPath)
return p
}