aboutsummaryrefslogtreecommitdiff
path: root/cmd/incremental_javac_input/incremental_javac_input_lib
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/incremental_javac_input/incremental_javac_input_lib')
-rw-r--r--cmd/incremental_javac_input/incremental_javac_input_lib/Android.bp30
-rw-r--r--cmd/incremental_javac_input/incremental_javac_input_lib/generate_incremental_input.go290
-rw-r--r--cmd/incremental_javac_input/incremental_javac_input_lib/generate_incremental_input_test.go699
3 files changed, 1019 insertions, 0 deletions
diff --git a/cmd/incremental_javac_input/incremental_javac_input_lib/Android.bp b/cmd/incremental_javac_input/incremental_javac_input_lib/Android.bp
new file mode 100644
index 000000000..42219a222
--- /dev/null
+++ b/cmd/incremental_javac_input/incremental_javac_input_lib/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+ name: "soong-cmd-incremental_javac_input-lib",
+ pkgPath: "android/soong/cmd/incremental_javac_input/incremental_javac_input_lib",
+ deps: [
+ "blueprint-pathtools",
+ "soong-cmd-find_input_delta-lib",
+ "golang-dependency-mapper-protoimpl",
+ ],
+ srcs: [
+ "generate_incremental_input.go",
+ ],
+}
diff --git a/cmd/incremental_javac_input/incremental_javac_input_lib/generate_incremental_input.go b/cmd/incremental_javac_input/incremental_javac_input_lib/generate_incremental_input.go
new file mode 100644
index 000000000..fdc7c9466
--- /dev/null
+++ b/cmd/incremental_javac_input/incremental_javac_input_lib/generate_incremental_input.go
@@ -0,0 +1,290 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package incremental_javac_input_lib
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ fid_lib "android/soong/cmd/find_input_delta/find_input_delta_lib"
+ "github.com/google/blueprint/pathtools"
+ dependency_proto "go.dependencymapper/protoimpl"
+ "google.golang.org/protobuf/proto"
+)
+
+var fileSepRegex = regexp.MustCompile("[^[:space:]]+")
+
+type UsageMap struct {
+ FilePath string
+
+ Usages []string
+
+ IsDependencyToAll bool
+
+ GeneratedClasses []string
+}
+
+func GenerateIncrementalInput(classDir, srcs, deps, javacTarget, srcDeps, localHeaderJars string) (err error) {
+ incInputPath := javacTarget + ".inc.rsp"
+ removedClassesPath := javacTarget + ".rem.rsp"
+ inputPcState := javacTarget + ".input.pc_state"
+ depsPcState := javacTarget + ".deps.pc_state"
+ headersPcState := javacTarget + ".headers.pc_state"
+
+ var classesForRemoval []string
+ var incAllSources bool
+
+ // Read the srcRspFile contents
+ srcList := readRspFile(srcs)
+ // run find_input_delta, save [add + ch] as a []string, and [del] as another []string
+ addF, delF, chF := findInputDelta(srcList, inputPcState, javacTarget)
+ var incInputList []string
+ incInputList = append(incInputList, addF...)
+ incInputList = append(incInputList, chF...)
+
+ // check if deps have changed
+ depsList := readRspFile(deps)
+ if addD, delD, chD := findInputDelta(depsList, depsPcState, javacTarget); len(addD)+len(delD)+len(chD) > 0 {
+ incAllSources = true
+ }
+
+ // If we are not doing a partial compile, we can just return the full list.
+ // We can do this earlier as well, but allowing findInputDelta to run outputs
+ // the changed sources to build metrics as well as maintains the state of
+ // inputs we require if the partialCompile is switched on again.
+ if !usePartialCompile() {
+ // Remove the output class directory to prevent any stale files
+ os.RemoveAll(classDir)
+ return writeOutput(incInputPath, removedClassesPath, srcList, classesForRemoval)
+ }
+
+ // if the output directory of javac which will contain .class files is not present, include all sources
+ if !dirExists(classDir) {
+ incAllSources = true
+ }
+
+ // if javacTarget does not exist, we can include all sources
+ if incAllSources || !fileExists(javacTarget) {
+ incAllSources = true
+ }
+
+ // if incInputList has the same size as srcList (all files touched), we can
+ // just include all sources
+ if incAllSources || len(incInputList) == len(srcList) {
+ incAllSources = true
+ }
+
+ // if dependencyMap does not exist, we can include all sources
+ if incAllSources || !fileExists(srcDeps) {
+ incAllSources = true
+ }
+
+ headersChanged := false
+ // if headers do not change, we can just keep the incInputList as is.
+ // Read the srcRspFile contents
+ headersList := readRspFile(localHeaderJars)
+ if addH, delH, chH := findInputDelta(headersList, headersPcState, javacTarget); len(addH)+len(delH)+len(chH) > 0 {
+ headersChanged = true
+ }
+
+ // use revDepsMap to find all usages, add them to output, alongside [add + ch] files
+ if fileExists(srcDeps) {
+ usageMap, _ := generateUsageMap(srcDeps)
+ // if including all sources, no need to check the usageMap
+ if headersChanged && !incAllSources {
+ incInputList, incAllSources = getUsages(usageMap, incInputList, delF, headersChanged)
+ }
+ // use usageMap to add all classes that were generated from removed files.
+ classesForRemoval = generateRemovalList(usageMap, delF, classDir)
+ }
+
+ if incAllSources {
+ incInputList = srcList
+ }
+
+ // write the output to output path(s)
+ return writeOutput(incInputPath, removedClassesPath, incInputList, classesForRemoval)
+}
+
+// Checks if Full Compile is enabled or not
+func usePartialCompile() bool {
+ usePartialCompileVar := os.Getenv("SOONG_USE_PARTIAL_COMPILE")
+ if usePartialCompileVar == "true" {
+ return true
+ }
+ return false
+}
+
+// Returns the list of files that use added, modified or deleted files.
+// Returns whether to include all src Files in incremental src set
+func getUsages(usageMap map[string]UsageMap, modifiedFiles, deletedFiles []string, headersChanged bool) ([]string, bool) {
+ usagesSet := make(map[string]bool)
+
+ // First add all the modified files in the output
+ for _, incInput := range modifiedFiles {
+ usagesSet[incInput] = true
+ }
+ // Add all the usages of modified + deleted files
+ for _, modFile := range append(modifiedFiles, deletedFiles...) {
+ if um, exists := usageMap[modFile]; exists {
+ if um.IsDependencyToAll {
+ return nil, true
+ }
+ if headersChanged {
+ for _, usage := range um.Usages {
+ usagesSet[usage] = true
+ }
+ }
+ }
+ }
+
+ var usages []string
+ for usage := range usagesSet {
+ usages = append(usages, usage)
+ }
+ return usages, false
+}
+
+// Returns the list of class files to be removed, as a result of deleting a source file.
+func generateRemovalList(usageMap map[string]UsageMap, delFiles []string, classesDir string) []string {
+ var classesForRemoval []string
+ for _, delFile := range delFiles {
+ if _, exists := usageMap[delFile]; exists {
+ for _, generatedClass := range usageMap[delFile].GeneratedClasses {
+ classesForRemoval = append(classesForRemoval, filepath.Join(classesDir, generatedClass))
+ }
+ }
+ }
+ return classesForRemoval
+}
+
+// Generates the usage map, by reading the supplied dependency map as a proto
+// Throws error if the map is unparsable.
+func generateUsageMap(srcDeps string) (map[string]UsageMap, error) {
+ var message = &dependency_proto.FileDependencyList{}
+
+ usageMapSet := make(map[string]UsageMap)
+
+ data, err := os.ReadFile(srcDeps)
+ if err != nil && errors.Is(err, fs.ErrNotExist) {
+ fmt.Println("err: ", err)
+ panic(err)
+ }
+ err = proto.Unmarshal(data, message)
+ if err != nil {
+ fmt.Println("err: ", err)
+ panic(err)
+ }
+ for _, dep := range message.FileDependency {
+ addUsageMapIfNotPresent(usageMapSet, *dep.FilePath)
+ for _, depV := range dep.FileDependencies {
+ addUsageMapIfNotPresent(usageMapSet, depV)
+ updatedUsageMap := usageMapSet[depV]
+ updatedUsageMap.Usages = append(updatedUsageMap.Usages, *dep.FilePath)
+ usageMapSet[depV] = updatedUsageMap
+ }
+ updatedUsageMap := usageMapSet[*dep.FilePath]
+ updatedUsageMap.IsDependencyToAll = *dep.IsDependencyToAll
+ updatedUsageMap.GeneratedClasses = dep.GeneratedClasses
+ usageMapSet[*dep.FilePath] = updatedUsageMap
+ }
+ return usageMapSet, nil
+}
+
+func addUsageMapIfNotPresent(usageMapSet map[string]UsageMap, key string) {
+ if _, exists := usageMapSet[key]; !exists {
+ usageMap := UsageMap{
+ FilePath: key,
+ Usages: []string{},
+ IsDependencyToAll: false,
+ GeneratedClasses: []string{},
+ }
+ usageMapSet[key] = usageMap
+ }
+}
+
+func fileExists(filePath string) bool {
+ if file, err := os.Open(filePath); err != nil {
+ if os.IsNotExist(err) {
+ return false
+ }
+ panic(err)
+ } else {
+ file.Close()
+ }
+ return true
+}
+
+func dirExists(dirPath string) bool {
+ if _, err := os.Stat(dirPath); err == nil || !os.IsNotExist(err) {
+ return true
+ }
+ return false
+}
+
+func readRspFile(rspFile string) (list []string) {
+ data, err := os.ReadFile(rspFile)
+ if err != nil {
+ panic(err)
+ }
+ list = append(list, fileSepRegex.FindAllString(string(data), -1)...)
+ return list
+}
+
+// Writes incInput and classesForRemoval to output paths
+func writeOutput(incInputPath, removedClassesPath string, incInputList, classesForRemoval []string) (err error) {
+ err = pathtools.WriteFileIfChanged(incInputPath, []byte(strings.Join(incInputList, "\n")), 0644)
+ if err != nil {
+ return err
+ }
+ return pathtools.WriteFileIfChanged(removedClassesPath, []byte(strings.Join(classesForRemoval, "\n")), 0644)
+}
+
+// Computes the diff of the inputs provided, saving the temp state in the
+// priorStateFile.
+func findInputDelta(inputs []string, priorStateFile, target string) ([]string, []string, []string) {
+ newStateFile := priorStateFile + ".new"
+ fileList, err := fid_lib.GenerateFileList(target, priorStateFile, newStateFile, inputs, false, fid_lib.OsFs)
+ if err != nil {
+ panic(err)
+ }
+ return flattenChanges(fileList)
+}
+
+// Flattens the output of find_input_delta for javac's consumption.
+func flattenChanges(root *fid_lib.FileList) ([]string, []string, []string) {
+ var allAdditions []string
+ var allDeletions []string
+ var allChangedFiles []string
+
+ for _, addition := range root.Additions {
+ allAdditions = append(allAdditions, addition)
+ }
+
+ for _, del := range root.Deletions {
+ allDeletions = append(allDeletions, del)
+ }
+
+ for _, ch := range root.Changes {
+ allChangedFiles = append(allChangedFiles, ch.Name)
+ }
+
+ return allAdditions, allDeletions, allChangedFiles
+}
diff --git a/cmd/incremental_javac_input/incremental_javac_input_lib/generate_incremental_input_test.go b/cmd/incremental_javac_input/incremental_javac_input_lib/generate_incremental_input_test.go
new file mode 100644
index 000000000..99ea804fd
--- /dev/null
+++ b/cmd/incremental_javac_input/incremental_javac_input_lib/generate_incremental_input_test.go
@@ -0,0 +1,699 @@
+package incremental_javac_input_lib
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+
+ fid_lib "android/soong/cmd/find_input_delta/find_input_delta_lib"
+ dependency_proto "go.dependencymapper/protoimpl"
+ "google.golang.org/protobuf/proto"
+)
+
+// --- Tests for `getUsages` ---
+func TestGetUsages(t *testing.T) {
+ usageMap := map[string]UsageMap{
+ "file1.java": {Usages: []string{"file3.java", "file4.java"}},
+ "file2.java": {Usages: []string{"file3.java"}},
+ "file3.java": {Usages: []string{}},
+ "fileAll.java": {Usages: []string{}, IsDependencyToAll: true},
+ }
+
+ testCases := []struct {
+ name string
+ modifiedFiles []string
+ deletedFiles []string
+ expected []string
+ expectedAll bool
+ }{
+ {
+ name: "Basic",
+ modifiedFiles: []string{"file1.java"},
+ deletedFiles: []string{"file2.java"},
+ expected: []string{"file1.java", "file3.java", "file4.java"}, // file3 is used by both
+ expectedAll: false,
+ },
+ {
+ name: "Empty",
+ modifiedFiles: []string{},
+ deletedFiles: []string{},
+ expected: nil,
+ expectedAll: false,
+ },
+ {
+ name: "NonExistentFile",
+ modifiedFiles: []string{"nonexistent.java"},
+ deletedFiles: []string{},
+ expected: []string{"nonexistent.java"},
+ expectedAll: false,
+ },
+ {
+ name: "DependencyToAll",
+ modifiedFiles: []string{"file1.java"},
+ deletedFiles: []string{"fileAll.java"},
+ expected: nil,
+ expectedAll: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ actual, all := getUsages(usageMap, tc.modifiedFiles, tc.deletedFiles, true)
+ if all != tc.expectedAll {
+ t.Errorf("getUsages() all sources; expected %v, got %v", tc.expectedAll, all)
+ }
+
+ // Sort for consistent comparison, as map iteration order isn't guaranteed
+ sort.Strings(actual)
+ sort.Strings(tc.expected)
+ if !reflect.DeepEqual(actual, tc.expected) {
+ t.Errorf("getUsages(); expected %v, got %v", tc.expected, actual)
+ }
+ })
+ }
+}
+
+// --- Tests for `generateRemovalList` ---
+func TestGenerateRemovalList(t *testing.T) {
+ usageMap := map[string]UsageMap{
+ "file1.java": {GeneratedClasses: []string{"Class1", "Class2"}},
+ "file2.java": {GeneratedClasses: []string{"Class3"}},
+ "file3.java": {},
+ }
+
+ testCases := []struct {
+ name string
+ classDir string
+ delFiles []string
+ expected []string
+ }{
+ {"Basic", "out/classes", []string{"file1.java", "file2.java"}, []string{"out/classes/Class1", "out/classes/Class2", "out/classes/Class3"}},
+ {"Empty", "out/classes", []string{}, nil},
+ {"NonExistent", "out/classes", []string{"nonexistent.java"}, nil},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ actual := generateRemovalList(usageMap, tc.delFiles, tc.classDir)
+ sort.Strings(actual)
+ sort.Strings(tc.expected)
+ if !reflect.DeepEqual(actual, tc.expected) {
+ t.Errorf("generateRemovalList(); expected %v, got %v", tc.expected, actual)
+ }
+ })
+ }
+}
+
+// --- Tests for `generateUsageMap` ---
+func TestGenerateUsageMap(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ // 1. Test with a valid proto file.
+ protoFile := filepath.Join(tmpDir, "deps.pb")
+ createProtoFile(t, protoFile)
+
+ usageMap, err := generateUsageMap(protoFile)
+ if err != nil {
+ t.Fatalf("generateUsageMap() returned an error: %v", err)
+ }
+
+ expectedUsageMap := map[string]UsageMap{
+ "file1.java": {FilePath: "file1.java", Usages: []string{}, IsDependencyToAll: false, GeneratedClasses: []string{"ClassA", "ClassB"}},
+ "file2.java": {FilePath: "file2.java", Usages: []string{"file1.java"}, IsDependencyToAll: true, GeneratedClasses: []string{"ClassC"}},
+ "file3.java": {FilePath: "file3.java", Usages: []string{"file1.java"}, IsDependencyToAll: false, GeneratedClasses: []string{"ClassD"}},
+ }
+
+ if !reflect.DeepEqual(usageMap, expectedUsageMap) {
+ t.Errorf("generateUsageMap() returned unexpected map.\nGot: %+v\nWant:%+v", usageMap, expectedUsageMap)
+ }
+
+ // 2. Test with a non-existent file (should panic)
+ t.Run("Panic on non-existent proto", func(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil { //should have panicked
+ t.Errorf("generateUsageMap() did not panic on non-existent proto")
+ }
+ }()
+ _, _ = generateUsageMap("nonexistent.pb")
+ })
+
+ // 3. Test with an invalid proto file (should panic)
+ invalidProtoFile := filepath.Join(tmpDir, "invalid.pb")
+ writeFile(t, invalidProtoFile, "This is not a valid proto file") // Create invalid file
+ t.Run("Panic on invalid proto file", func(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil { //should have panicked
+ t.Errorf("generateUsageMap() did not panic on invalid proto")
+ }
+ }()
+ _, _ = generateUsageMap(invalidProtoFile)
+ })
+}
+
+// --- Tests for `readRspFile` ---
+func TestReadRspFile(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ testCases := []struct {
+ name string
+ content string
+ expected []string
+ }{
+ {"Empty", "", nil},
+ {"SingleLine", "file1.java", []string{"file1.java"}},
+ {"MultipleLines", "file1.java\nfile2.java\nfile3.java", []string{"file1.java", "file2.java", "file3.java"}},
+ {"WithSpaces", " file1.java \n file2.java ", []string{"file1.java", "file2.java"}}, //Should be trimmed.
+ {"WithEmptyLines", "file1.java\n\nfile2.java", []string{"file1.java", "file2.java"}},
+ {"WithCarriageReturn", "file1.java\r\nfile2.java", []string{"file1.java", "file2.java"}},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ rspFile := filepath.Join(tmpDir, "test.rsp")
+ writeFile(t, rspFile, tc.content)
+
+ actual := readRspFile(rspFile)
+ if !reflect.DeepEqual(actual, tc.expected) {
+ t.Errorf("readRspFile(); expected %v, got %v", tc.expected, actual)
+ }
+ })
+ }
+
+ //Test for panic
+ t.Run("Panic on non-existent file", func(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil { //should have panicked
+ t.Errorf("readRspFile() did not panic on non-existent file")
+ }
+ }()
+ _ = readRspFile("nonexistent_file.rsp")
+ })
+}
+
+// --- Tests for `flattenChanges` ---
+
+func TestFlattenChanges(t *testing.T) {
+ fileList := &fid_lib.FileList{
+ Additions: []string{"add1.java", "add2.java"},
+ Deletions: []string{"del1.java"},
+ Changes: []fid_lib.FileList{
+ {Name: "change1.java"},
+ {Name: "change2.java"},
+ },
+ }
+
+ expectedAdditions := []string{"add1.java", "add2.java"}
+ expectedDeletions := []string{"del1.java"}
+ expectedChanges := []string{"change1.java", "change2.java"}
+
+ actualAdditions, actualDeletions, actualChanges := flattenChanges(fileList)
+
+ // Sort for consistent comparison
+ sort.Strings(expectedAdditions)
+ sort.Strings(actualAdditions)
+ sort.Strings(expectedDeletions)
+ sort.Strings(actualDeletions)
+ sort.Strings(expectedChanges)
+ sort.Strings(actualChanges)
+
+ if !reflect.DeepEqual(actualAdditions, expectedAdditions) {
+ t.Errorf("flattenChanges() additions; expected %v, got %v", expectedAdditions, actualAdditions)
+ }
+ if !reflect.DeepEqual(actualDeletions, expectedDeletions) {
+ t.Errorf("flattenChanges() deletions; expected %v, got %v", expectedDeletions, actualDeletions)
+ }
+ if !reflect.DeepEqual(actualChanges, expectedChanges) {
+ t.Errorf("flattenChanges() changes; expected %v, got %v", expectedChanges, actualChanges)
+ }
+}
+
+// --- Tests for `GenerateIncrementalInput` ---
+
+func TestGenerateIncrementalInput(t *testing.T) {
+ // Set the environment variable to enable inc-compilation
+ t.Setenv("SOONG_USE_PARTIAL_COMPILE", "true")
+
+ // Shared setup for all subtests
+ tf := newTestFixture(t)
+ // No need for top-level defer os.RemoveAll(tmpDir) because t.TempDir() handles it
+
+ // --- Subtest: Initial Full Compile ---
+ t.Run("InitialFullCompile", func(t *testing.T) {
+ // Arrange (already done by newTestFixture)
+
+ // Act
+ tf.runGenerator()
+
+ // Assert
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s\n%s", tf.JavaFile1, tf.JavaFile2, tf.JavaFile3), // All files included initially
+ tf.remOutputPath(),
+ "", // No removals initially
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - One File Modified ---
+ t.Run("Incremental_OneFileModified", func(t *testing.T) {
+ // Arrange: Modify one file (ensure timestamp changes)
+ modifyFile(t, tf.JavaFile3, "Incremental_OneFileModified")
+
+ // Act
+ tf.runGenerator()
+
+ // Assert: Only the modified file should be in inc.rsp
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s", tf.JavaFile3),
+ tf.remOutputPath(),
+ "", // No removals
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - One File and Header Modified ---
+ t.Run("Incremental_FileAndHeaderModified", func(t *testing.T) {
+ // Arrange: Modify a different file and the header jar
+ modifyFile(t, tf.JavaFile3, "Incremental_FileAndHeaderModified")
+ modifyFile(t, tf.HeaderJar, "Incremental_FileAndHeaderModified")
+
+ // Act
+ tf.runGenerator()
+
+ // Assert: All source files and their usages should be in inc.rsp
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s", tf.JavaFile1, tf.JavaFile3),
+ tf.remOutputPath(),
+ "", // No removals
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - Dependency Change ---
+ t.Run("Incremental_DependencyChanged", func(t *testing.T) {
+ // Arrange: Modify the DepJar
+ modifyFile(t, tf.DepJar, "Incremental_DependencyChanged")
+
+ // Act
+ tf.runGenerator()
+
+ // Assert: All source files should be in inc.rsp
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s\n%s", tf.JavaFile1, tf.JavaFile2, tf.JavaFile3),
+ tf.remOutputPath(),
+ "", // No removals
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - File which is dependency to all files is Changed ---
+ t.Run("Incremental_DependencyToAllChanged", func(t *testing.T) {
+ // Arrange: Modify the DepsRspFile or JavaSrcDeps proto
+ modifyFile(t, tf.JavaFile2, "Incremental_DependencyToAllChanged")
+
+ // Act
+ tf.runGenerator()
+
+ // Assert: All source files should be in inc.rsp
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s", tf.JavaFile2),
+ tf.remOutputPath(),
+ "", // No removals
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - File which is dependency to all files is changed along with headers---
+ t.Run("Incremental_DependencyToAllChangedWithHeaders", func(t *testing.T) {
+ // Arrange: Modify the DepsRspFile or JavaSrcDeps proto
+ modifyFile(t, tf.JavaFile2, "Incremental_DependencyToAllChangedWithHeader")
+ modifyFile(t, tf.HeaderJar, "Incremental_DependencyToAllChangedWithHeader")
+
+ // Act
+ tf.runGenerator()
+
+ // Assert: All source files should be in inc.rsp
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s\n%s", tf.JavaFile1, tf.JavaFile2, tf.JavaFile3),
+ tf.remOutputPath(),
+ "", // No removals
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - One File Deleted, Header Modified ---
+ t.Run("Incremental_FileDeletedHeaderModified", func(t *testing.T) {
+ // Arrange: Delete one file and modify header
+ deleteFile(t, tf.JavaFile3, tf.SrcRspFile)
+ // Modify Headers
+ modifyFile(t, tf.HeaderJar, "Incremental_FileDeletedHeaderModified")
+
+ // Act
+ tf.runGenerator()
+
+ // Assert: Check usages of deleted file in inc.rsp, and class files
+ // corresponding to deleted files in rem.rsp
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s", tf.JavaFile1), // usages of deleted file
+ tf.remOutputPath(),
+ filepath.Join(tf.ClassDir, "org.another.ClassD"), // class name corresponding to the deleted file path
+ )
+ tf.savePriorState() // Save state if needed for subsequent tests
+ })
+}
+
+func TestGenerateIncrementalInputPartialCompileOff(t *testing.T) {
+ // Set the environment variable to disable inc-compilation
+ t.Setenv("SOONG_USE_PARTIAL_COMPILE", "")
+
+ // Shared setup for all subtests
+ tf := newTestFixture(t)
+ // No need for top-level defer os.RemoveAll(tmpDir) because t.TempDir() handles it
+
+ // --- Subtest: Initial Full Compile ---
+ t.Run("InitialFullCompile", func(t *testing.T) {
+ // Arrange (already done by newTestFixture)
+
+ // Act
+ tf.runGenerator()
+
+ // Assert
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s\n%s", tf.JavaFile1, tf.JavaFile2, tf.JavaFile3), // All files included initially
+ tf.remOutputPath(),
+ "", // No removals initially
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - One File Modified, should add all files ---
+ t.Run("Incremental_OneFileModified_AddsAllFiles", func(t *testing.T) {
+ // Arrange: Modify one file (ensure timestamp changes)
+ modifyFile(t, tf.JavaFile3, "Incremental_OneFileModified")
+
+ // Act
+ tf.runGenerator()
+
+ // Assert: Only the modified file should be in inc.rsp
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s\n%s", tf.JavaFile1, tf.JavaFile2, tf.JavaFile3),
+ tf.remOutputPath(),
+ "", // No removals
+ )
+ tf.savePriorState()
+ })
+}
+
+// --- Test Fixture Setup ---
+// Struct to hold common test file paths
+type testFixture struct {
+ t *testing.T
+ tmpDir string
+ ClassDir string
+ SrcRspFile string
+ DepsRspFile string
+ JavacTargetJar string
+ JavaSrcDeps string
+ HeadersRspFile string
+ JavaFile1 string
+ JavaFile2 string
+ JavaFile3 string
+ DepJar string
+ HeaderJar string
+}
+
+// newTestFixture creates the temporary directory and necessary files
+func newTestFixture(t *testing.T) *testFixture {
+ tmpDir := t.TempDir() // Use t.TempDir for automatic cleanup
+
+ // Create dummy files needed for the tests
+ fixture := &testFixture{
+ t: t,
+ tmpDir: tmpDir,
+ ClassDir: filepath.Join(tmpDir, "classes"),
+ SrcRspFile: filepath.Join(tmpDir, "sources.rsp"),
+ DepsRspFile: filepath.Join(tmpDir, "deps.rsp"),
+ JavacTargetJar: filepath.Join(tmpDir, "output.jar"),
+ JavaSrcDeps: filepath.Join(tmpDir, "srcdeps.pb"), // Example proto file path
+ HeadersRspFile: filepath.Join(tmpDir, "localHeaders.rsp"),
+ JavaFile1: filepath.Join(tmpDir, "src/com/example/ClassA.java"),
+ JavaFile2: filepath.Join(tmpDir, "src/com/example/ClassC.java"),
+ JavaFile3: filepath.Join(tmpDir, "src/org/another/ClassD.java"), // Example different package
+ DepJar: filepath.Join(tmpDir, "deps.jar"),
+ HeaderJar: filepath.Join(tmpDir, "headers.jar"),
+ }
+
+ // Create directories and initial file contents
+ createDir(t, filepath.Dir(fixture.JavaFile1))
+ createDir(t, filepath.Dir(fixture.JavaFile3))
+ createDir(t, fixture.ClassDir)
+
+ writeFile(t, fixture.JavaFile1, "package com.example; class File1 {}")
+ writeFile(t, fixture.JavaFile2, "package com.example; class File2 {}")
+ writeFile(t, fixture.JavaFile3, "package org.another; class ClassD {}")
+
+ writeFile(t, fixture.DepJar, "Dep jar")
+ writeFile(t, fixture.HeaderJar, "Header jar")
+
+ writeFile(t, fixture.SrcRspFile, fmt.Sprintf("%s\n%s\n%s", fixture.JavaFile1, fixture.JavaFile2, fixture.JavaFile3))
+ writeFile(t, fixture.DepsRspFile, fmt.Sprintf("%s", fixture.DepJar))
+ writeFile(t, fixture.HeadersRspFile, fmt.Sprintf("%s", fixture.HeaderJar))
+ writeFile(t, fixture.JavacTargetJar, "Javac Jar")
+ writeFile(t, fixture.JavaSrcDeps, "")
+ createProtoFileWithActualPaths(t, fixture.JavaSrcDeps, fixture.JavaFile1, fixture.JavaFile2, fixture.JavaFile3)
+
+ return fixture
+}
+
+// runGenerator calls GenerateIncrementalInput for the testFixture
+func (tf *testFixture) runGenerator() {
+ // Small delay often needed for filesystem timestamp granularity
+ time.Sleep(15 * time.Millisecond)
+ err := GenerateIncrementalInput(tf.ClassDir, tf.SrcRspFile, tf.DepsRspFile, tf.JavacTargetJar, tf.JavaSrcDeps, tf.HeadersRspFile)
+ if err != nil {
+ tf.t.Fatalf("GenerateIncrementalInput() returned an error: %v", err)
+ }
+}
+
+// returns incOutputPath for testFixture
+func (tf *testFixture) incOutputPath() string {
+ return tf.JavacTargetJar + ".inc.rsp"
+}
+
+// returns remOutputPath for testFixture
+func (tf *testFixture) remOutputPath() string {
+ return tf.JavacTargetJar + ".rem.rsp"
+}
+
+// Verifies the test output against expected output
+func checkOutput(t *testing.T, incOutputPath, expectedIncContent, remOutputPath, expectedRemContent string) {
+ checkFileContent(t, incOutputPath, expectedIncContent)
+ checkFileContent(t, remOutputPath, expectedRemContent)
+}
+
+// Helper to check if the content of a file matches the expected content (order insensitive)
+func checkFileContent(t *testing.T, filePath, expectedContent string) {
+ contentBytes, err := os.ReadFile(filePath)
+ if err != nil {
+ t.Fatalf("Failed to read output file %q: %v", filePath, err)
+ }
+ actualContent := strings.TrimSpace(string(contentBytes))
+
+ actualLines := strings.Split(actualContent, "\n")
+ expectedLines := strings.Split(expectedContent, "\n")
+ sort.Strings(actualLines)
+ sort.Strings(expectedLines)
+ actualContent = strings.Join(actualLines, "\n")
+ expectedContent = strings.Join(expectedLines, "\n")
+
+ if actualContent != expectedContent {
+ t.Errorf("Unexpected content in %q.\nGot:\n%s\nWant:\n%s", filePath, actualContent, expectedContent)
+ }
+}
+
+// Helper to save prior state
+func (tf *testFixture) savePriorState() {
+ tf.t.Helper()
+ // Implement your logic to save the necessary state files
+ // e.g., copy *.pc_state.new to *.pc_state
+ inputStateNew := tf.JavacTargetJar + ".input.pc_state.new"
+ inputState := tf.JavacTargetJar + ".input.pc_state"
+ depsStateNew := tf.JavacTargetJar + ".deps.pc_state.new"
+ depsState := tf.JavacTargetJar + ".deps.pc_state"
+ headerStateNew := tf.JavacTargetJar + ".headers.pc_state.new"
+ headerState := tf.JavacTargetJar + ".headers.pc_state"
+
+ os.Rename(inputStateNew, inputState)
+ os.Rename(depsStateNew, depsState)
+ os.Rename(headerStateNew, headerState)
+}
+
+// --- File Create/Mod/Delete helpers ---
+
+func createDir(t *testing.T, dirPath string) {
+ t.Helper()
+ err := os.MkdirAll(dirPath, 0755)
+ if err != nil {
+ t.Fatalf("Failed to create directory %q: %v", dirPath, err)
+ }
+}
+
+func writeFile(t *testing.T, filePath, content string) {
+ t.Helper()
+ err := os.WriteFile(filePath, []byte(content), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write file %q: %v", filePath, err)
+ }
+}
+
+func modifyFile(t *testing.T, filePath, newContentSuffix string) {
+ t.Helper()
+ // Append suffix to ensure modification time changes reliably
+ contentBytes, err := os.ReadFile(filePath)
+ if err != nil && !os.IsNotExist(err) { // Allow modification even if file was deleted before
+ t.Fatalf("Failed to read file for modification %q: %v", filePath, err)
+ }
+ newContent := string(contentBytes) + "// " + newContentSuffix
+ writeFile(t, filePath, newContent)
+}
+
+func deleteFile(t *testing.T, filePath, srcRspFilePath string) {
+ t.Helper()
+ err := os.Remove(filePath)
+ if err != nil && !os.IsNotExist(err) { // Ignore error if already deleted
+ t.Fatalf("Failed to delete file %q: %v", filePath, err)
+ }
+ // Also update the source rsp file!
+ updateSrcRsp(t, srcRspFilePath, filePath, true)
+}
+
+func updateSrcRsp(t *testing.T, rspPath, filePath string, remove bool) {
+ t.Helper()
+ contentBytes, err := os.ReadFile(rspPath)
+ if err != nil {
+ t.Fatalf("Failed to read source rsp file %q: %v", rspPath, err)
+ }
+ lines := strings.Split(string(contentBytes), "\n")
+ var newLines []string
+ found := false
+ for _, line := range lines {
+ trimmedLine := strings.TrimSpace(line)
+ if trimmedLine == "" {
+ continue
+ }
+ if trimmedLine == filePath {
+ found = true
+ if !remove { // Keep it if not removing
+ newLines = append(newLines, trimmedLine)
+ }
+ } else {
+ newLines = append(newLines, trimmedLine)
+ }
+ }
+ // Add back if it wasn't found and we are not removing (e.g., restoring)
+ if !found && !remove {
+ newLines = append(newLines, filePath)
+ }
+
+ writeFile(t, rspPath, strings.Join(newLines, "\n"))
+}
+
+// --- ProtoFile Creation helpers ---
+
+func createProtoFile(t *testing.T, filePath string) string {
+ t.Helper()
+
+ dep1 := &dependency_proto.FileDependency{
+ FilePath: proto.String("file1.java"),
+ FileDependencies: []string{"file2.java", "file3.java"},
+ IsDependencyToAll: proto.Bool(false),
+ GeneratedClasses: []string{"ClassA", "ClassB"},
+ }
+ dep2 := &dependency_proto.FileDependency{
+ FilePath: proto.String("file2.java"),
+ FileDependencies: []string{},
+ IsDependencyToAll: proto.Bool(true),
+ GeneratedClasses: []string{"ClassC"},
+ }
+ dep3 := &dependency_proto.FileDependency{
+ FilePath: proto.String("file3.java"),
+ FileDependencies: []string{},
+ IsDependencyToAll: proto.Bool(false),
+ GeneratedClasses: []string{"ClassD"},
+ }
+
+ message := &dependency_proto.FileDependencyList{
+ FileDependency: []*dependency_proto.FileDependency{dep1, dep2, dep3},
+ }
+
+ data, err := proto.Marshal(message)
+ if err != nil {
+ t.Fatalf("Failed to marshal proto message: %v", err)
+ }
+
+ if err := os.WriteFile(filePath, data, 0644); err != nil {
+ t.Fatalf("Failed to write proto file: %v", err)
+ }
+
+ return filePath
+}
+
+func createProtoFileWithActualPaths(t *testing.T, protoFilePath, javaFile1, javaFile2, javaFile3 string) string {
+ t.Helper()
+
+ dep1 := &dependency_proto.FileDependency{
+ FilePath: proto.String(javaFile1),
+ FileDependencies: []string{javaFile2, javaFile3},
+ IsDependencyToAll: proto.Bool(false),
+ GeneratedClasses: []string{"src/com/example/ClassA", "src/com/example/ClassB"},
+ }
+ dep2 := &dependency_proto.FileDependency{
+ FilePath: proto.String(javaFile2),
+ FileDependencies: []string{},
+ IsDependencyToAll: proto.Bool(true),
+ GeneratedClasses: []string{"src/com/example/ClassC"},
+ }
+ dep3 := &dependency_proto.FileDependency{
+ FilePath: proto.String(javaFile3),
+ FileDependencies: []string{},
+ IsDependencyToAll: proto.Bool(false),
+ GeneratedClasses: []string{"org.another.ClassD"},
+ }
+
+ message := &dependency_proto.FileDependencyList{
+ FileDependency: []*dependency_proto.FileDependency{dep1, dep2, dep3},
+ }
+
+ data, err := proto.Marshal(message)
+ if err != nil {
+ t.Fatalf("Failed to marshal proto message: %v", err)
+ }
+
+ if err := os.WriteFile(protoFilePath, data, 0644); err != nil {
+ t.Fatalf("Failed to write proto file: %v", err)
+ }
+
+ return protoFilePath
+}