| // 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 |
| } |