github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/internal/pkg/downloadmismatch/diff.go (about) 1 // Copyright 2023 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package downloadmismatch downloads compare build mismatch outputs. 16 package downloadmismatch 17 18 import ( 19 "bytes" 20 "fmt" 21 "os" 22 "os/exec" 23 "path/filepath" 24 ) 25 26 const ( 27 stringsSuffix = ".strings" 28 diffFileName = "compare_action.diff" 29 ) 30 31 func runAndWriteOutput(outFp string, cmd *exec.Cmd) error { 32 cmd.Env = append(os.Environ()) 33 var out bytes.Buffer 34 var stderr bytes.Buffer 35 cmd.Stderr = &stderr 36 cmd.Stdout = &out 37 38 // Ignore the command return code, non-zero code from e.g. diff is expected. 39 cmd.Run() 40 41 if stderr.Len() > 0 { 42 fmt.Printf("%v stderr: %q\n", cmd.String(), stderr.String()) 43 } 44 return os.WriteFile(outFp, out.Bytes(), os.ModePerm) 45 } 46 47 func linuxStrings(fp string) (string, error) { 48 outFp := fp + stringsSuffix 49 return outFp, runAndWriteOutput(outFp, exec.Command("strings", fp)) 50 } 51 52 // DiffReadableString diffs the strings outputs of the two inputs and store the diff outputs in outFp. 53 func DiffReadableString(outFp, fp1, fp2 string) error { 54 fp1Strings, err1 := linuxStrings(fp1) 55 fp2Strings, err2 := linuxStrings(fp2) 56 if err1 != nil { 57 fmt.Printf("error reading %v: %v", fp1, err1) 58 return err1 59 } 60 if err2 != nil { 61 fmt.Printf("error reading %v: %v", fp1, err1) 62 return err2 63 } 64 return runAndWriteOutput(outFp, exec.Command("diff", fp1Strings, fp2Strings)) 65 } 66 67 // Visit each action directory and diff remote vs local outputs. If one mismatch has multiple retries, 68 // we only diff the first remote vs first local output. 69 func visitAction(curPath string) error { 70 f, err := os.Open(curPath) 71 if err != nil { 72 return err 73 } 74 outputs, err := f.Readdir(0) 75 if err != nil { 76 return err 77 } 78 for _, output := range outputs { 79 // TODO: output can be directories if we modify 80 // download.go to download directories. 81 if err := visitOutput(filepath.Join(curPath, output.Name())); err != nil { 82 return err 83 } 84 } 85 return nil 86 } 87 88 func visitOutput(outputPath string) error { 89 localOutputs, err := os.ReadDir(filepath.Join(outputPath, LocalOutputDir)) 90 if err != nil { 91 return err 92 } 93 remoteOutputs, err := os.ReadDir(filepath.Join(outputPath, RemoteOutputDir)) 94 if err != nil { 95 return err 96 } 97 if len(localOutputs) != 1 || len(remoteOutputs) != 1 { 98 return fmt.Errorf("Missing or more than 1 local/remote output") 99 } 100 101 DiffReadableString(filepath.Join(outputPath, diffFileName), filepath.Join(outputPath, LocalOutputDir, localOutputs[0].Name()), filepath.Join(outputPath, RemoteOutputDir, remoteOutputs[0].Name())) 102 return nil 103 } 104 105 // DiffOutputDir visits the directory storing compare build outputs downloaded by this package. 106 func DiffOutputDir(outputDir string) error { 107 dirs, err := os.ReadDir(outputDir) 108 if err != nil { 109 return err 110 } 111 112 for _, f := range dirs { 113 if f.IsDir() { 114 curPath := filepath.Join(outputDir, f.Name()) 115 if err := visitAction(curPath); err != nil { 116 return err 117 } 118 } else { 119 return fmt.Errorf("Error iterating output Directory, %s is not directory", f.Name()) 120 } 121 } 122 return nil 123 }