soong_ui: Add build actions commands in soong_ui.

Add the following build actions {BUILD_MODULES_IN_A_DIRECTORY,
BUILD_MODULES_IN_DIRECTORIES} in soong_ui config so the bash code version of
build commands (m, mm, mma, mmm, mmma) in build/make/envsetup.sh can be deprecated.
This is to allow up to date bug fixes on the build commands.

Bug: b/130049705
Test: Unit test cases
Change-Id: I772db1d4e9c1da5273374d1994eb5e8f17cd52f2
diff --git a/ui/build/config_test.go b/ui/build/config_test.go
index 242e3af..dede4d1 100644
--- a/ui/build/config_test.go
+++ b/ui/build/config_test.go
@@ -17,6 +17,10 @@
 import (
 	"bytes"
 	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
 	"reflect"
 	"strings"
 	"testing"
@@ -173,3 +177,877 @@
 		})
 	}
 }
+
+func TestConfigCheckTopDir(t *testing.T) {
+	ctx := testContext()
+	buildRootDir := filepath.Dir(srcDirFileCheck)
+	expectedErrStr := fmt.Sprintf("Current working directory must be the source tree. %q not found.", srcDirFileCheck)
+
+	tests := []struct {
+		// ********* Setup *********
+		// Test description.
+		description string
+
+		// ********* Action *********
+		// If set to true, the build root file is created.
+		rootBuildFile bool
+
+		// The current path where Soong is being executed.
+		path string
+
+		// ********* Validation *********
+		// Expecting error and validate the error string against expectedErrStr.
+		wantErr bool
+	}{{
+		description:   "current directory is the root source tree",
+		rootBuildFile: true,
+		path:          ".",
+		wantErr:       false,
+	}, {
+		description:   "one level deep in the source tree",
+		rootBuildFile: true,
+		path:          "1",
+		wantErr:       true,
+	}, {
+		description:   "very deep in the source tree",
+		rootBuildFile: true,
+		path:          "1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/7",
+		wantErr:       true,
+	}, {
+		description:   "outside of source tree",
+		rootBuildFile: false,
+		path:          "1/2/3/4/5",
+		wantErr:       true,
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			defer logger.Recover(func(err error) {
+				if !tt.wantErr {
+					t.Fatalf("Got unexpected error: %v", err)
+				}
+				if expectedErrStr != err.Error() {
+					t.Fatalf("expected %s, got %s", expectedErrStr, err.Error())
+				}
+			})
+
+			// Create the root source tree.
+			rootDir, err := ioutil.TempDir("", "")
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer os.RemoveAll(rootDir)
+
+			// Create the build root file. This is to test if topDir returns an error if the build root
+			// file does not exist.
+			if tt.rootBuildFile {
+				dir := filepath.Join(rootDir, buildRootDir)
+				if err := os.MkdirAll(dir, 0755); err != nil {
+					t.Errorf("failed to create %s directory: %v", dir, err)
+				}
+				f := filepath.Join(rootDir, srcDirFileCheck)
+				if err := ioutil.WriteFile(f, []byte{}, 0644); err != nil {
+					t.Errorf("failed to create file %s: %v", f, err)
+				}
+			}
+
+			// Next block of code is to set the current directory.
+			dir := rootDir
+			if tt.path != "" {
+				dir = filepath.Join(dir, tt.path)
+				if err := os.MkdirAll(dir, 0755); err != nil {
+					t.Errorf("failed to create %s directory: %v", dir, err)
+				}
+			}
+			curDir, err := os.Getwd()
+			if err != nil {
+				t.Fatalf("failed to get the current directory: %v", err)
+			}
+			defer func() { os.Chdir(curDir) }()
+
+			if err := os.Chdir(dir); err != nil {
+				t.Fatalf("failed to change directory to %s: %v", dir, err)
+			}
+
+			checkTopDir(ctx)
+		})
+	}
+}
+
+func TestConfigConvertToTarget(t *testing.T) {
+	tests := []struct {
+		// ********* Setup *********
+		// Test description.
+		description string
+
+		// ********* Action *********
+		// The current directory where Soong is being executed.
+		dir string
+
+		// The current prefix string to be pre-appended to the target.
+		prefix string
+
+		// ********* Validation *********
+		// The expected target to be invoked in ninja.
+		expectedTarget string
+	}{{
+		description:    "one level directory in source tree",
+		dir:            "test1",
+		prefix:         "MODULES-IN-",
+		expectedTarget: "MODULES-IN-test1",
+	}, {
+		description:    "multiple level directories in source tree",
+		dir:            "test1/test2/test3/test4",
+		prefix:         "GET-INSTALL-PATH-IN-",
+		expectedTarget: "GET-INSTALL-PATH-IN-test1-test2-test3-test4",
+	}}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			target := convertToTarget(tt.dir, tt.prefix)
+			if target != tt.expectedTarget {
+				t.Errorf("expected %s, got %s for target", tt.expectedTarget, target)
+			}
+		})
+	}
+}
+
+func setTop(t *testing.T, dir string) func() {
+	curDir, err := os.Getwd()
+	if err != nil {
+		t.Fatalf("failed to get current directory: %v", err)
+	}
+	if err := os.Chdir(dir); err != nil {
+		t.Fatalf("failed to change directory to top dir %s: %v", dir, err)
+	}
+	return func() { os.Chdir(curDir) }
+}
+
+func createBuildFiles(t *testing.T, topDir string, buildFiles []string) {
+	for _, buildFile := range buildFiles {
+		buildFile = filepath.Join(topDir, buildFile)
+		if err := ioutil.WriteFile(buildFile, []byte{}, 0644); err != nil {
+			t.Errorf("failed to create file %s: %v", buildFile, err)
+		}
+	}
+}
+
+func createDirectories(t *testing.T, topDir string, dirs []string) {
+	for _, dir := range dirs {
+		dir = filepath.Join(topDir, dir)
+		if err := os.MkdirAll(dir, 0755); err != nil {
+			t.Errorf("failed to create %s directory: %v", dir, err)
+		}
+	}
+}
+
+func TestConfigGetTargets(t *testing.T) {
+	ctx := testContext()
+	tests := []struct {
+		// ********* Setup *********
+		// Test description.
+		description string
+
+		// Directories that exist in the source tree.
+		dirsInTrees []string
+
+		// Build files that exists in the source tree.
+		buildFiles []string
+
+		// ********* Action *********
+		// Directories passed in to soong_ui.
+		dirs []string
+
+		// Current directory that the user executed the build action command.
+		curDir string
+
+		// ********* Validation *********
+		// Expected targets from the function.
+		expectedTargets []string
+
+		// Expected build from the build system.
+		expectedBuildFiles []string
+
+		// Expecting error from running test case.
+		errStr string
+	}{{
+		description:        "one target dir specified",
+		dirsInTrees:        []string{"0/1/2/3"},
+		buildFiles:         []string{"0/1/2/3/Android.bp"},
+		dirs:               []string{"1/2/3"},
+		curDir:             "0",
+		expectedTargets:    []string{"MODULES-IN-0-1-2-3"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk"},
+	}, {
+		description: "one target dir specified, build file does not exist",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{},
+		dirs:        []string{"1/2/3"},
+		curDir:      "0",
+		errStr:      "Build file not found for 0/1/2/3 directory",
+	}, {
+		description: "one target dir specified, invalid targets specified",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{},
+		dirs:        []string{"1/2/3:t1:t2"},
+		curDir:      "0",
+		errStr:      "1/2/3:t1:t2 not in proper directory:target1,target2,... format (\":\" was specified more than once)",
+	}, {
+		description:        "one target dir specified, no targets specified but has colon",
+		dirsInTrees:        []string{"0/1/2/3"},
+		buildFiles:         []string{"0/1/2/3/Android.bp"},
+		dirs:               []string{"1/2/3:"},
+		curDir:             "0",
+		expectedTargets:    []string{"MODULES-IN-0-1-2-3"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk"},
+	}, {
+		description:        "one target dir specified, two targets specified",
+		dirsInTrees:        []string{"0/1/2/3"},
+		buildFiles:         []string{"0/1/2/3/Android.bp"},
+		dirs:               []string{"1/2/3:t1,t2"},
+		curDir:             "0",
+		expectedTargets:    []string{"t1", "t2"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk"},
+	}, {
+		description: "one target dir specified, no targets and has a comma",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{"0/1/2/3/Android.bp"},
+		dirs:        []string{"1/2/3:,"},
+		curDir:      "0",
+		errStr:      "0/1/2/3 not in proper directory:target1,target2,... format",
+	}, {
+		description: "one target dir specified, improper targets defined",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{"0/1/2/3/Android.bp"},
+		dirs:        []string{"1/2/3:,t1"},
+		curDir:      "0",
+		errStr:      "0/1/2/3 not in proper directory:target1,target2,... format",
+	}, {
+		description: "one target dir specified, blank target",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{"0/1/2/3/Android.bp"},
+		dirs:        []string{"1/2/3:t1,"},
+		curDir:      "0",
+		errStr:      "0/1/2/3 not in proper directory:target1,target2,... format",
+	}, {
+		description:        "one target dir specified, many targets specified",
+		dirsInTrees:        []string{"0/1/2/3"},
+		buildFiles:         []string{"0/1/2/3/Android.bp"},
+		dirs:               []string{"1/2/3:t1,t2,t3,t4,t5,t6,t7,t8,t9,t10"},
+		curDir:             "0",
+		expectedTargets:    []string{"t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8", "t9", "t10"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk"},
+	}, {
+		description: "one target dir specified, one target specified, build file does not exist",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{},
+		dirs:        []string{"1/2/3:t1"},
+		curDir:      "0",
+		errStr:      "Build file not found for 0/1/2/3 directory",
+	}, {
+		description: "one target dir specified, one target specified, build file not in target dir",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{"0/1/2/Android.mk"},
+		dirs:        []string{"1/2/3:t1"},
+		curDir:      "0",
+		errStr:      "Couldn't locate a build file from 0/1/2/3 directory",
+	}, {
+		description:        "one target dir specified, build file not in target dir",
+		dirsInTrees:        []string{"0/1/2/3"},
+		buildFiles:         []string{"0/1/2/Android.mk"},
+		dirs:               []string{"1/2/3"},
+		curDir:             "0",
+		expectedTargets:    []string{"MODULES-IN-0-1-2"},
+		expectedBuildFiles: []string{"0/1/2/Android.mk"},
+	}, {
+		description:        "multiple targets dir specified, targets specified",
+		dirsInTrees:        []string{"0/1/2/3", "0/3/4"},
+		buildFiles:         []string{"0/1/2/3/Android.bp", "0/3/4/Android.mk"},
+		dirs:               []string{"1/2/3:t1,t2", "3/4:t3,t4,t5"},
+		curDir:             "0",
+		expectedTargets:    []string{"t1", "t2", "t3", "t4", "t5"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk", "0/3/4/Android.mk"},
+	}, {
+		description:        "multiple targets dir specified, one directory has targets specified",
+		dirsInTrees:        []string{"0/1/2/3", "0/3/4"},
+		buildFiles:         []string{"0/1/2/3/Android.bp", "0/3/4/Android.mk"},
+		dirs:               []string{"1/2/3:t1,t2", "3/4"},
+		curDir:             "0",
+		expectedTargets:    []string{"t1", "t2", "MODULES-IN-0-3-4"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk", "0/3/4/Android.mk"},
+	}, {
+		description: "two dirs specified, only one dir exist",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{"0/1/2/3/Android.mk"},
+		dirs:        []string{"1/2/3:t1", "3/4"},
+		curDir:      "0",
+		errStr:      "couldn't find directory 0/3/4",
+	}, {
+		description:        "multiple targets dirs specified at root source tree",
+		dirsInTrees:        []string{"0/1/2/3", "0/3/4"},
+		buildFiles:         []string{"0/1/2/3/Android.bp", "0/3/4/Android.mk"},
+		dirs:               []string{"0/1/2/3:t1,t2", "0/3/4"},
+		curDir:             ".",
+		expectedTargets:    []string{"t1", "t2", "MODULES-IN-0-3-4"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk", "0/3/4/Android.mk"},
+	}, {
+		description: "no directories specified",
+		dirsInTrees: []string{"0/1/2/3", "0/3/4"},
+		buildFiles:  []string{"0/1/2/3/Android.bp", "0/3/4/Android.mk"},
+		dirs:        []string{},
+		curDir:      ".",
+	}}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			defer logger.Recover(func(err error) {
+				if tt.errStr == "" {
+					t.Fatalf("Got unexpected error: %v", err)
+				}
+				if tt.errStr != err.Error() {
+					t.Errorf("expected %s, got %s", tt.errStr, err.Error())
+				}
+			})
+
+			// Create the root source tree.
+			topDir, err := ioutil.TempDir("", "")
+			if err != nil {
+				t.Fatalf("failed to create temp dir: %v", err)
+			}
+			defer os.RemoveAll(topDir)
+
+			createDirectories(t, topDir, tt.dirsInTrees)
+			createBuildFiles(t, topDir, tt.buildFiles)
+			r := setTop(t, topDir)
+			defer r()
+
+			targets, buildFiles := getTargetsFromDirs(ctx, tt.curDir, tt.dirs, "MODULES-IN-")
+			if !reflect.DeepEqual(targets, tt.expectedTargets) {
+				t.Errorf("expected %v, got %v for targets", tt.expectedTargets, targets)
+			}
+			if !reflect.DeepEqual(buildFiles, tt.expectedBuildFiles) {
+				t.Errorf("expected %v, got %v for build files", tt.expectedBuildFiles, buildFiles)
+			}
+
+			// If the execution reached here and there was an expected error code, the unit test case failed.
+			if tt.errStr != "" {
+				t.Errorf("expecting error %s", tt.errStr)
+			}
+		})
+	}
+}
+
+func TestConfigFindBuildFile(t *testing.T) {
+	ctx := testContext()
+
+	tests := []struct {
+		// ********* Setup *********
+		// Test description.
+		description string
+
+		// Array of build files to create in dir.
+		buildFiles []string
+
+		// ********* Action *********
+		// Directory to create, also the base directory is where findBuildFile is invoked.
+		dir string
+
+		// ********* Validation *********
+		// Expected build file path to find.
+		expectedBuildFile string
+	}{{
+		description:       "build file exists at leaf directory",
+		buildFiles:        []string{"1/2/3/Android.bp"},
+		dir:               "1/2/3",
+		expectedBuildFile: "1/2/3/Android.mk",
+	}, {
+		description:       "build file exists in all directory paths",
+		buildFiles:        []string{"1/Android.mk", "1/2/Android.mk", "1/2/3/Android.mk"},
+		dir:               "1/2/3",
+		expectedBuildFile: "1/2/3/Android.mk",
+	}, {
+		description:       "build file does not exist in all directory paths",
+		buildFiles:        []string{},
+		dir:               "1/2/3",
+		expectedBuildFile: "",
+	}, {
+		description:       "build file exists only at top directory",
+		buildFiles:        []string{"Android.bp"},
+		dir:               "1/2/3",
+		expectedBuildFile: "",
+	}, {
+		description:       "build file exist in a subdirectory",
+		buildFiles:        []string{"1/2/Android.bp"},
+		dir:               "1/2/3",
+		expectedBuildFile: "1/2/Android.mk",
+	}, {
+		description:       "build file exists in a subdirectory",
+		buildFiles:        []string{"1/Android.mk"},
+		dir:               "1/2/3",
+		expectedBuildFile: "1/Android.mk",
+	}, {
+		description:       "top directory",
+		buildFiles:        []string{"Android.bp"},
+		dir:               ".",
+		expectedBuildFile: "",
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			defer logger.Recover(func(err error) {
+				t.Fatalf("Got unexpected error: %v", err)
+			})
+
+			topDir, err := ioutil.TempDir("", "")
+			if err != nil {
+				t.Fatalf("failed to create temp dir: %v", err)
+			}
+			defer os.RemoveAll(topDir)
+
+			if tt.dir != "" {
+				createDirectories(t, topDir, []string{tt.dir})
+			}
+
+			createBuildFiles(t, topDir, tt.buildFiles)
+
+			curDir, err := os.Getwd()
+			if err != nil {
+				t.Fatalf("Could not get working directory: %v", err)
+			}
+			defer func() { os.Chdir(curDir) }()
+			if err := os.Chdir(topDir); err != nil {
+				t.Fatalf("Could not change top dir to %s: %v", topDir, err)
+			}
+
+			buildFile := findBuildFile(ctx, tt.dir)
+			if buildFile != tt.expectedBuildFile {
+				t.Errorf("expected %q, got %q for build file", tt.expectedBuildFile, buildFile)
+			}
+		})
+	}
+}
+
+func TestConfigSplitArgs(t *testing.T) {
+	tests := []struct {
+		// ********* Setup *********
+		// Test description.
+		description string
+
+		// ********* Action *********
+		// Arguments passed in to soong_ui.
+		args []string
+
+		// ********* Validation *********
+		// Expected newArgs list after extracting the directories.
+		expectedNewArgs []string
+
+		// Expected directories
+		expectedDirs []string
+	}{{
+		description:     "flags but no directories specified",
+		args:            []string{"showcommands", "-j", "-k"},
+		expectedNewArgs: []string{"showcommands", "-j", "-k"},
+		expectedDirs:    []string{},
+	}, {
+		description:     "flags and one directory specified",
+		args:            []string{"snod", "-j", "dir:target1,target2"},
+		expectedNewArgs: []string{"snod", "-j"},
+		expectedDirs:    []string{"dir:target1,target2"},
+	}, {
+		description:     "flags and directories specified",
+		args:            []string{"dist", "-k", "dir1", "dir2:target1,target2"},
+		expectedNewArgs: []string{"dist", "-k"},
+		expectedDirs:    []string{"dir1", "dir2:target1,target2"},
+	}, {
+		description:     "only directories specified",
+		args:            []string{"dir1", "dir2", "dir3:target1,target2"},
+		expectedNewArgs: []string{},
+		expectedDirs:    []string{"dir1", "dir2", "dir3:target1,target2"},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			args, dirs := splitArgs(tt.args)
+			if !reflect.DeepEqual(tt.expectedNewArgs, args) {
+				t.Errorf("expected %v, got %v for arguments", tt.expectedNewArgs, args)
+			}
+			if !reflect.DeepEqual(tt.expectedDirs, dirs) {
+				t.Errorf("expected %v, got %v for directories", tt.expectedDirs, dirs)
+			}
+		})
+	}
+}
+
+type envVar struct {
+	name  string
+	value string
+}
+
+type buildActionTestCase struct {
+	// ********* Setup *********
+	// Test description.
+	description string
+
+	// Directories that exist in the source tree.
+	dirsInTrees []string
+
+	// Build files that exists in the source tree.
+	buildFiles []string
+
+	// ********* Action *********
+	// Arguments passed in to soong_ui.
+	args []string
+
+	// Directory where the build action was invoked.
+	curDir string
+
+	// WITH_TIDY_ONLY environment variable specified.
+	tidyOnly string
+
+	// ********* Validation *********
+	// Expected arguments to be in Config instance.
+	expectedArgs []string
+
+	// Expected environment variables to be set.
+	expectedEnvVars []envVar
+}
+
+func testGetConfigArgs(t *testing.T, tt buildActionTestCase, action BuildAction, buildDependencies bool) {
+	ctx := testContext()
+
+	// Environment variables to set it to blank on every test case run.
+	resetEnvVars := []string{
+		"ONE_SHOT_MAKEFILE",
+		"WITH_TIDY_ONLY",
+	}
+
+	for _, name := range resetEnvVars {
+		if err := os.Unsetenv(name); err != nil {
+			t.Fatalf("failed to unset environment variable %s: %v", name, err)
+		}
+	}
+	if tt.tidyOnly != "" {
+		if err := os.Setenv("WITH_TIDY_ONLY", tt.tidyOnly); err != nil {
+			t.Errorf("failed to set WITH_TIDY_ONLY to %s: %v", tt.tidyOnly, err)
+		}
+	}
+
+	// Create the root source tree.
+	topDir, err := ioutil.TempDir("", "")
+	if err != nil {
+		t.Fatalf("failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(topDir)
+
+	createDirectories(t, topDir, tt.dirsInTrees)
+	createBuildFiles(t, topDir, tt.buildFiles)
+
+	r := setTop(t, topDir)
+	defer r()
+
+	// The next block is to create the root build file.
+	rootBuildFileDir := filepath.Dir(srcDirFileCheck)
+	if err := os.MkdirAll(rootBuildFileDir, 0755); err != nil {
+		t.Fatalf("Failed to create %s directory: %v", rootBuildFileDir, err)
+	}
+
+	if err := ioutil.WriteFile(srcDirFileCheck, []byte{}, 0644); err != nil {
+		t.Fatalf("failed to create %s file: %v", srcDirFileCheck, err)
+	}
+
+	args := getConfigArgs(action, tt.curDir, buildDependencies, ctx, tt.args)
+	if !reflect.DeepEqual(tt.expectedArgs, args) {
+		t.Fatalf("expected %v, got %v for config arguments", tt.expectedArgs, args)
+	}
+
+	for _, env := range tt.expectedEnvVars {
+		if val := os.Getenv(env.name); val != env.value {
+			t.Errorf("expecting %s, got %s for environment variable %s", env.value, val, env.name)
+		}
+	}
+}
+
+// TODO: Remove this test case once mm shell build command has been deprecated.
+func TestGetConfigArgsBuildModulesInDirecotoryNoDeps(t *testing.T) {
+	tests := []buildActionTestCase{{
+		description:  "normal execution in a directory",
+		dirsInTrees:  []string{"0/1/2"},
+		buildFiles:   []string{"0/1/2/Android.mk"},
+		args:         []string{"-j", "-k", "showcommands", "fake-module"},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"-j", "-k", "showcommands", "fake-module", "MODULES-IN-0-1-2"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/Android.mk"}},
+	}, {
+		description:  "makefile in parent directory",
+		dirsInTrees:  []string{"0/1/2"},
+		buildFiles:   []string{"0/1/Android.mk"},
+		args:         []string{},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"MODULES-IN-0-1"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/Android.mk"}},
+	}, {
+		description:  "build file not found",
+		dirsInTrees:  []string{"0/1/2"},
+		buildFiles:   []string{},
+		args:         []string{},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"MODULES-IN-0-1-2"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/Android.mk"}},
+	}, {
+		description:  "build action executed at root directory",
+		dirsInTrees:  []string{},
+		buildFiles:   []string{},
+		args:         []string{},
+		curDir:       ".",
+		tidyOnly:     "",
+		expectedArgs: []string{},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: ""}},
+	}, {
+		description:  "GET-INSTALL-PATH specified,",
+		dirsInTrees:  []string{"0/1/2"},
+		buildFiles:   []string{"0/1/Android.mk"},
+		args:         []string{"GET-INSTALL-PATH"},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"GET-INSTALL-PATH-IN-0-1"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/Android.mk"}},
+	}, {
+		description:  "tidy only environment variable specified,",
+		dirsInTrees:  []string{"0/1/2"},
+		buildFiles:   []string{"0/1/Android.mk"},
+		args:         []string{"GET-INSTALL-PATH"},
+		curDir:       "0/1/2",
+		tidyOnly:     "true",
+		expectedArgs: []string{"tidy_only"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/Android.mk"}},
+	}}
+	for _, tt := range tests {
+		t.Run("build action BUILD_MODULES_IN_DIR without their dependencies, "+tt.description, func(t *testing.T) {
+			testGetConfigArgs(t, tt, BUILD_MODULES_IN_A_DIRECTORY, false)
+		})
+	}
+}
+
+func TestGetConfigArgsBuildModulesInDirectory(t *testing.T) {
+	tests := []buildActionTestCase{{
+		description:     "normal execution in a directory",
+		dirsInTrees:     []string{"0/1/2"},
+		buildFiles:      []string{"0/1/2/Android.mk"},
+		args:            []string{"fake-module"},
+		curDir:          "0/1/2",
+		tidyOnly:        "",
+		expectedArgs:    []string{"fake-module", "MODULES-IN-0-1-2"},
+		expectedEnvVars: []envVar{},
+	}, {
+		description:     "build file in parent directory",
+		dirsInTrees:     []string{"0/1/2"},
+		buildFiles:      []string{"0/1/Android.mk"},
+		args:            []string{},
+		curDir:          "0/1/2",
+		tidyOnly:        "",
+		expectedArgs:    []string{"MODULES-IN-0-1"},
+		expectedEnvVars: []envVar{},
+	},
+		{
+			description:     "build file in parent directory, multiple module names passed in",
+			dirsInTrees:     []string{"0/1/2"},
+			buildFiles:      []string{"0/1/Android.mk"},
+			args:            []string{"fake-module1", "fake-module2", "fake-module3"},
+			curDir:          "0/1/2",
+			tidyOnly:        "",
+			expectedArgs:    []string{"fake-module1", "fake-module2", "fake-module3", "MODULES-IN-0-1"},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "build file in 2nd level parent directory",
+			dirsInTrees:     []string{"0/1/2"},
+			buildFiles:      []string{"0/Android.bp"},
+			args:            []string{},
+			curDir:          "0/1/2",
+			tidyOnly:        "",
+			expectedArgs:    []string{"MODULES-IN-0"},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "build action executed at root directory",
+			dirsInTrees:     []string{},
+			buildFiles:      []string{},
+			args:            []string{},
+			curDir:          ".",
+			tidyOnly:        "",
+			expectedArgs:    []string{},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "build file not found - no error is expected to return",
+			dirsInTrees:     []string{"0/1/2"},
+			buildFiles:      []string{},
+			args:            []string{},
+			curDir:          "0/1/2",
+			tidyOnly:        "",
+			expectedArgs:    []string{"MODULES-IN-0-1-2"},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "GET-INSTALL-PATH specified,",
+			dirsInTrees:     []string{"0/1/2"},
+			buildFiles:      []string{"0/1/Android.mk"},
+			args:            []string{"GET-INSTALL-PATH", "-j", "-k", "GET-INSTALL-PATH"},
+			curDir:          "0/1/2",
+			tidyOnly:        "",
+			expectedArgs:    []string{"-j", "-k", "GET-INSTALL-PATH-IN-0-1"},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "tidy only environment variable specified,",
+			dirsInTrees:     []string{"0/1/2"},
+			buildFiles:      []string{"0/1/Android.mk"},
+			args:            []string{"GET-INSTALL-PATH"},
+			curDir:          "0/1/2",
+			tidyOnly:        "true",
+			expectedArgs:    []string{"tidy_only"},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "normal execution in root directory with args",
+			dirsInTrees:     []string{},
+			buildFiles:      []string{},
+			args:            []string{"-j", "-k", "fake_module"},
+			curDir:          "",
+			tidyOnly:        "",
+			expectedArgs:    []string{"-j", "-k", "fake_module"},
+			expectedEnvVars: []envVar{},
+		}}
+	for _, tt := range tests {
+		t.Run("build action BUILD_MODULES_IN_DIR, "+tt.description, func(t *testing.T) {
+			testGetConfigArgs(t, tt, BUILD_MODULES_IN_A_DIRECTORY, true)
+		})
+	}
+}
+
+// TODO: Remove this test case once mmm shell build command has been deprecated.
+func TestGetConfigArgsBuildModulesInDirectoriesNoDeps(t *testing.T) {
+	tests := []buildActionTestCase{{
+		description:  "normal execution in a directory",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"3.1/:t1,t2", "3.2/:t3,t4", "3.3/:t5,t6"},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"t1", "t2", "t3", "t4", "t5", "t6"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/3.1/Android.mk 0/1/2/3.2/Android.mk 0/1/2/3.3/Android.mk"}},
+	}, {
+		description:  "GET-INSTALL-PATH specified",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"GET-INSTALL-PATH", "3.1/", "3.2/", "3.3/:t6"},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"GET-INSTALL-PATH-IN-0-1-2-3.1", "GET-INSTALL-PATH-IN-0-1-2-3.2", "t6"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/3.1/Android.mk 0/1/2/3.2/Android.mk 0/1/2/3.3/Android.mk"}},
+	}, {
+		description:  "tidy only environment variable specified",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"GET-INSTALL-PATH", "3.1/", "3.2/", "3.3/:t6"},
+		curDir:       "0/1/2",
+		tidyOnly:     "1",
+		expectedArgs: []string{"tidy_only"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/3.1/Android.mk 0/1/2/3.2/Android.mk 0/1/2/3.3/Android.mk"}},
+	}, {
+		description:  "normal execution from top dir directory",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"0/1/2/3.1", "0/1/2/3.2/:t3,t4", "0/1/2/3.3/:t5,t6"},
+		curDir:       ".",
+		tidyOnly:     "",
+		expectedArgs: []string{"MODULES-IN-0-1-2-3.1", "t3", "t4", "t5", "t6"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/3.1/Android.mk 0/1/2/3.2/Android.mk 0/1/2/3.3/Android.mk"}},
+	}}
+	for _, tt := range tests {
+		t.Run("build action BUILD_MODULES_IN_DIRS_NO_DEPS, "+tt.description, func(t *testing.T) {
+			testGetConfigArgs(t, tt, BUILD_MODULES_IN_DIRECTORIES, false)
+		})
+	}
+}
+
+func TestGetConfigArgsBuildModulesInDirectories(t *testing.T) {
+	tests := []buildActionTestCase{{
+		description:  "normal execution in a directory",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"3.1/", "3.2/", "3.3/"},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"MODULES-IN-0-1-2-3.1", "MODULES-IN-0-1-2-3.2", "MODULES-IN-0-1-2-3.3"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: ""}},
+	}, {
+		description:  "GET-INSTALL-PATH specified",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/Android.bp"},
+		args:         []string{"GET-INSTALL-PATH", "2/3.1/", "2/3.2", "3"},
+		curDir:       "0/1",
+		tidyOnly:     "",
+		expectedArgs: []string{"GET-INSTALL-PATH-IN-0-1-2-3.1", "GET-INSTALL-PATH-IN-0-1-2-3.2", "GET-INSTALL-PATH-IN-0-1"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: ""}},
+	}, {
+		description:  "tidy only environment variable specified",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"GET-INSTALL-PATH", "3.1/", "3.2/", "3.3"},
+		curDir:       "0/1/2",
+		tidyOnly:     "1",
+		expectedArgs: []string{"tidy_only"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: ""}},
+	}, {
+		description:  "normal execution from top dir directory",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/3", "0/2"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/3/Android.bp", "0/2/Android.bp"},
+		args:         []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/3", "0/2"},
+		curDir:       ".",
+		tidyOnly:     "",
+		expectedArgs: []string{"MODULES-IN-0-1-2-3.1", "MODULES-IN-0-1-2-3.2", "MODULES-IN-0-1-3", "MODULES-IN-0-2"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: ""}},
+	}}
+	for _, tt := range tests {
+		t.Run("build action BUILD_MODULES_IN_DIRS, "+tt.description, func(t *testing.T) {
+			testGetConfigArgs(t, tt, BUILD_MODULES_IN_DIRECTORIES, true)
+		})
+	}
+}