]> ruderich.org/simon Gitweb - safcm/safcm.git/blobdiff - frontend/changes_test.go
safcm: move sync_changes.go and term.go to frontend package
[safcm/safcm.git] / frontend / changes_test.go
diff --git a/frontend/changes_test.go b/frontend/changes_test.go
new file mode 100644 (file)
index 0000000..02a95c4
--- /dev/null
@@ -0,0 +1,1234 @@
+// Copyright (C) 2021  Simon Ruderich
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package frontend
+
+import (
+       "io/fs"
+       "testing"
+
+       "ruderich.org/simon/safcm"
+       "ruderich.org/simon/safcm/testutil"
+)
+
+func TestFormatChanges(t *testing.T) {
+       tests := []struct {
+               name   string
+               dryRun bool
+               quiet  bool
+               isTTY  bool
+               resp   safcm.MsgSyncResp
+               exp    string
+       }{
+
+               // Just a few basic tests and border cases; see the other
+               // tests for more detailed tests of each format function
+
+               {
+                       "no changes",
+                       false,
+                       false,
+                       false,
+                       safcm.MsgSyncResp{},
+                       "no changes",
+               },
+
+               {
+                       "changes",
+                       false,
+                       false,
+                       false,
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "created",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  "user",
+                                                       Uid:   1000,
+                                                       Group: "group",
+                                                       Gid:   2000,
+                                               },
+                                       },
+                               },
+                               PackageChanges: []safcm.PackageChange{
+                                       {
+                                               Name: "package-one",
+                                       },
+                                       {
+                                               Name: "package-two",
+                                       },
+                               },
+                               ServiceChanges: []safcm.ServiceChange{
+                                       {
+                                               Name:    "service-one",
+                                               Started: true,
+                                       },
+                                       {
+                                               Name:    "service-two",
+                                               Enabled: true,
+                                       },
+                                       {
+                                               Name:    "service-three",
+                                               Started: true,
+                                               Enabled: true,
+                                       },
+                               },
+                               CommandChanges: []safcm.CommandChange{
+                                       {
+                                               Command: "fake command",
+                                               Output:  "fake output",
+                                       },
+                                       {
+                                               Command: "fake command with no output",
+                                       },
+                               },
+                       },
+                       "\nchanged 1 file(s):\n\"created\": created, file, user(1000) group(2000), 0644\n\ninstalled 2 package(s):\n\"package-one\"\n\"package-two\"\n\nmodified 3 service(s):\n\"service-one\": started\n\"service-two\": enabled\n\"service-three\": started, enabled\n\nexecuted 2 command(s):\n\"fake command\":\n   > fake output\n   > \\ No newline at end of file\n\"fake command with no output\"\n",
+               },
+
+               {
+                       "command changes only, dry-run",
+                       true,
+                       false,
+                       false,
+                       safcm.MsgSyncResp{
+                               CommandChanges: []safcm.CommandChange{
+                                       {
+                                               Command: "fake command",
+                                       },
+                                       {
+                                               Command: "fake command with no output",
+                                       },
+                                       {
+                                               Command: "fake command with newline",
+                                       },
+                                       {
+                                               Command: "fake command with more output",
+                                       },
+                                       {
+                                               Command: "fake failed command",
+                                       },
+                               },
+                       },
+                       "\nwill execute 5 command(s): (dry-run)\n\"fake command\"\n\"fake command with no output\"\n\"fake command with newline\"\n\"fake command with more output\"\n\"fake failed command\"\n",
+               },
+               {
+                       "command changes only, quiet & dry-run",
+                       true,
+                       true,
+                       false,
+                       safcm.MsgSyncResp{
+                               CommandChanges: []safcm.CommandChange{
+                                       {
+                                               Command: "fake command",
+                                       },
+                                       {
+                                               Command: "fake command with no output",
+                                       },
+                                       {
+                                               Command: "fake command with newline",
+                                       },
+                                       {
+                                               Command: "fake command with more output",
+                                       },
+                                       {
+                                               Command: "fake failed command",
+                                       },
+                               },
+                       },
+                       "will execute 5 command(s) (dry-run)\n",
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       c := Changes{
+                               DryRun: tc.dryRun,
+                               Quiet:  tc.quiet,
+                               IsTTY:  tc.isTTY,
+                       }
+
+                       res := c.FormatChanges(tc.resp)
+                       testutil.AssertEqual(t, "res", res, tc.exp)
+               })
+       }
+}
+
+func TestFormatFileChanges(t *testing.T) {
+       tests := []struct {
+               name    string
+               dryRun  bool
+               isTTY   bool
+               changes []safcm.FileChange
+               exp     string
+       }{
+
+               {
+                       "regular",
+                       false,
+                       false,
+                       []safcm.FileChange{
+                               {
+                                       Path:    "created: file",
+                                       Created: true,
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0644,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                               {
+                                       Path:    "created: link",
+                                       Created: true,
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  fs.ModeSymlink | 0777,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                               {
+                                       Path: "type change: file -> dir",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0751,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  fs.ModeDir | 0751,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       DataDiff: `@@ -1,2 +1 @@
+-content
+`,
+                               },
+                               {
+                                       Path: "user change",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user2",
+                                               Uid:   1001,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                               {
+                                       Path: "group change",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group2",
+                                               Gid:   2001,
+                                       },
+                               },
+                               {
+                                       Path: "mode change",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0750,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                               {
+                                       Path: "mode change (setuid)",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0755 | fs.ModeSetuid,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                               {
+                                       Path: "content change",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0644,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0644,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       DataDiff: `@@ -1,2 +1,2 @@
+-old content
++content
+`,
+                               },
+                               {
+                                       Path: "multiple changes",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0644,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  fs.ModeDir | 0755,
+                                               User:  "user2",
+                                               Uid:   1001,
+                                               Group: "group2",
+                                               Gid:   2001,
+                                       },
+                                       DataDiff: `@@ -1,2 +1 @@
+-content
+`,
+                               },
+                       },
+                       `changed 9 file(s):
+"created: file": created, file, user(1000) group(2000), 0644
+"created: link": created, symlink, user(1000) group(2000), 0777
+"type change: file -> dir": file -> dir
+   @@ -1,2 +1 @@
+   -content
+    
+"user change": user(1000) group(2000) -> user2(1001) group(2000)
+"group change": user(1000) group(2000) -> user(1000) group2(2001)
+"mode change": 0755 -> 0750
+"mode change (setuid)": 0755 -> 04755
+"content change":
+   @@ -1,2 +1,2 @@
+   -old content
+   +content
+    
+"multiple changes": file -> dir, user(1000) group(2000) -> user2(1001) group2(2001), 0644 -> 0755
+   @@ -1,2 +1 @@
+   -content
+    
+`,
+               },
+
+               {
+                       "regular (tty)",
+                       false,
+                       true,
+                       []safcm.FileChange{
+                               {
+                                       Path:    "created: file",
+                                       Created: true,
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0644,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                               {
+                                       Path:    "created: link",
+                                       Created: true,
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  fs.ModeSymlink | 0777,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                               {
+                                       Path: "type change: file -> dir",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0751,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  fs.ModeDir | 0751,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       DataDiff: `@@ -1,2 +1 @@
+-content
+`,
+                               },
+                               {
+                                       Path: "user change",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user2",
+                                               Uid:   1001,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                               {
+                                       Path: "group change",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group2",
+                                               Gid:   2001,
+                                       },
+                               },
+                               {
+                                       Path: "mode change",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0750,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                               {
+                                       Path: "mode change (setuid)",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0755,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0755 | fs.ModeSetuid,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                               {
+                                       Path: "content change",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0644,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0644,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       DataDiff: `@@ -1,2 +1,2 @@
+-old content
++content
+`,
+                               },
+                               {
+                                       Path: "multiple changes",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0644,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  fs.ModeDir | 0755,
+                                               User:  "user2",
+                                               Uid:   1001,
+                                               Group: "group2",
+                                               Gid:   2001,
+                                       },
+                                       DataDiff: `@@ -1,2 +1 @@
+-content
+`,
+                               },
+                       },
+                       "changed 9 file(s):\n\x1b[36m\"created: file\"\x1b[0m: \x1b[32mcreated\x1b[0m, file, user(1000) group(2000), 0644\n\x1b[36m\"created: link\"\x1b[0m: \x1b[32mcreated\x1b[0m, symlink, user(1000) group(2000), 0777\n\x1b[36m\"type change: file -> dir\"\x1b[0m: file -> dir\n   @@ -1,2 +1 @@\n\x1b[31m   -content\x1b[0m\n    \n\x1b[36m\"user change\"\x1b[0m: user(1000) group(2000) -> user2(1001) group(2000)\n\x1b[36m\"group change\"\x1b[0m: user(1000) group(2000) -> user(1000) group2(2001)\n\x1b[36m\"mode change\"\x1b[0m: 0755 -> 0750\n\x1b[36m\"mode change (setuid)\"\x1b[0m: 0755 -> 04755\n\x1b[36m\"content change\"\x1b[0m:\n   @@ -1,2 +1,2 @@\n\x1b[31m   -old content\x1b[0m\n\x1b[32m   +content\x1b[0m\n    \n\x1b[36m\"multiple changes\"\x1b[0m: file -> dir, user(1000) group(2000) -> user2(1001) group2(2001), 0644 -> 0755\n   @@ -1,2 +1 @@\n\x1b[31m   -content\x1b[0m\n    \n",
+               },
+
+               {
+                       "dry-run",
+                       true,
+                       false,
+                       []safcm.FileChange{
+                               {
+                                       Path:    "file",
+                                       Created: true,
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0644,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                       },
+                       `will change 1 file(s): (dry-run)
+"file": created, file, user(1000) group(2000), 0644
+`,
+               },
+
+               {
+                       "dry-run (tty)",
+                       true,
+                       true,
+                       []safcm.FileChange{
+                               {
+                                       Path:    "file",
+                                       Created: true,
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0644,
+                                               User:  "user",
+                                               Uid:   1000,
+                                               Group: "group",
+                                               Gid:   2000,
+                                       },
+                               },
+                       },
+                       "will change 1 file(s): (dry-run)\n\x1B[36m\"file\"\x1B[0m: \x1B[32mcreated\x1B[0m, file, user(1000) group(2000), 0644\n",
+               },
+
+               {
+                       "escaping",
+                       false,
+                       false,
+                       []safcm.FileChange{
+                               {
+                                       Path:    "\x00",
+                                       Created: true,
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0xFFFFFFFF,
+                                               User:  "\x01",
+                                               Uid:   -1,
+                                               Group: "\x02",
+                                               Gid:   -2,
+                                       },
+                                       DataDiff: "\x03",
+                               },
+                               {
+                                       Path: "\x00",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0x00000000,
+                                               User:  "\x01",
+                                               Uid:   -1,
+                                               Group: "\x02",
+                                               Gid:   -2,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0xFFFFFFFF,
+                                               User:  "\x03",
+                                               Uid:   -3,
+                                               Group: "\x04",
+                                               Gid:   -4,
+                                       },
+                                       DataDiff: "\x05",
+                               },
+                       },
+                       `changed 2 file(s):
+"\x00": created, invalid type dLDpSc?---------, \x01(-1) \x02(-2), 07777
+   \x03
+   \ No newline at end of file
+"\x00": file -> invalid type dLDpSc?---------, \x01(-1) \x02(-2) -> \x03(-3) \x04(-4), 0 -> 07777
+   \x05
+   \ No newline at end of file
+`,
+               },
+
+               {
+                       "escaping (tty)",
+                       false,
+                       true,
+                       []safcm.FileChange{
+                               {
+                                       Path:    "\x00",
+                                       Created: true,
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0xFFFFFFFF,
+                                               User:  "\x01",
+                                               Uid:   -1,
+                                               Group: "\x02",
+                                               Gid:   -2,
+                                       },
+                                       DataDiff: "\x03",
+                               },
+                               {
+                                       Path: "\x00",
+                                       Old: safcm.FileChangeInfo{
+                                               Mode:  0x00000000,
+                                               User:  "\x01",
+                                               Uid:   -1,
+                                               Group: "\x02",
+                                               Gid:   -2,
+                                       },
+                                       New: safcm.FileChangeInfo{
+                                               Mode:  0xFFFFFFFF,
+                                               User:  "\x03",
+                                               Uid:   -3,
+                                               Group: "\x04",
+                                               Gid:   -4,
+                                       },
+                                       DataDiff: "\x05",
+                               },
+                       },
+                       "changed 2 file(s):\n\x1b[36m\"\\x00\"\x1b[0m: \x1b[32mcreated\x1b[0m, invalid type dLDpSc?---------, \\x01(-1) \\x02(-2), 07777\n   \\x03\n   \\ No newline at end of file\n\x1b[36m\"\\x00\"\x1b[0m: file -> invalid type dLDpSc?---------, \\x01(-1) \\x02(-2) -> \\x03(-3) \\x04(-4), 0 -> 07777\n   \\x05\n   \\ No newline at end of file\n",
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       c := Changes{
+                               DryRun: tc.dryRun,
+                               IsTTY:  tc.isTTY,
+                       }
+
+                       res := c.FormatFileChanges(tc.changes)
+                       testutil.AssertEqual(t, "res", res, tc.exp)
+               })
+       }
+}
+
+func TestFormatPackageChanges(t *testing.T) {
+       tests := []struct {
+               name    string
+               dryRun  bool
+               isTTY   bool
+               changes []safcm.PackageChange
+               exp     string
+       }{
+
+               {
+                       "regular",
+                       false,
+                       false,
+                       []safcm.PackageChange{
+                               {
+                                       Name: "package-one",
+                               },
+                               {
+                                       Name: "package-two",
+                               },
+                       },
+                       `installed 2 package(s):
+"package-one"
+"package-two"
+`,
+               },
+
+               {
+                       "regular (tty)",
+                       false,
+                       true,
+                       []safcm.PackageChange{
+                               {
+                                       Name: "package-one",
+                               },
+                               {
+                                       Name: "package-two",
+                               },
+                       },
+                       "installed 2 package(s):\n\x1b[36m\"package-one\"\x1b[0m\n\x1b[36m\"package-two\"\x1b[0m\n",
+               },
+
+               {
+                       "dry-run",
+                       true,
+                       false,
+                       []safcm.PackageChange{
+                               {
+                                       Name: "package-one",
+                               },
+                               {
+                                       Name: "package-two",
+                               },
+                       },
+                       `will install 2 package(s): (dry-run)
+"package-one"
+"package-two"
+`,
+               },
+
+               {
+                       "dry-run (tty)",
+                       true,
+                       true,
+                       []safcm.PackageChange{
+                               {
+                                       Name: "package-one",
+                               },
+                               {
+                                       Name: "package-two",
+                               },
+                       },
+                       "will install 2 package(s): (dry-run)\n\x1b[36m\"package-one\"\x1b[0m\n\x1b[36m\"package-two\"\x1b[0m\n",
+               },
+
+               {
+                       "escaping",
+                       false,
+                       false,
+                       []safcm.PackageChange{
+                               {
+                                       Name: "\x00",
+                               },
+                       },
+                       `installed 1 package(s):
+"\x00"
+`,
+               },
+
+               {
+                       "escaping (tty)",
+                       false,
+                       true,
+                       []safcm.PackageChange{
+                               {
+                                       Name: "\x00",
+                               },
+                       },
+                       "installed 1 package(s):\n\x1b[36m\"\\x00\"\x1b[0m\n",
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       c := Changes{
+                               DryRun: tc.dryRun,
+                               IsTTY:  tc.isTTY,
+                       }
+
+                       res := c.FormatPackageChanges(tc.changes)
+                       testutil.AssertEqual(t, "res", res, tc.exp)
+               })
+       }
+}
+
+func TestFormatServiceChanges(t *testing.T) {
+       tests := []struct {
+               name    string
+               dryRun  bool
+               isTTY   bool
+               changes []safcm.ServiceChange
+               exp     string
+       }{
+
+               {
+                       "regular",
+                       false,
+                       false,
+                       []safcm.ServiceChange{
+                               {
+                                       Name:    "service-one",
+                                       Started: true,
+                               },
+                               {
+                                       Name:    "service-two",
+                                       Enabled: true,
+                               },
+                               {
+                                       Name:    "service-three",
+                                       Started: true,
+                                       Enabled: true,
+                               },
+                       },
+                       `modified 3 service(s):
+"service-one": started
+"service-two": enabled
+"service-three": started, enabled
+`,
+               },
+
+               {
+                       "regular (tty)",
+                       false,
+                       true,
+                       []safcm.ServiceChange{
+                               {
+                                       Name:    "service-one",
+                                       Started: true,
+                               },
+                               {
+                                       Name:    "service-two",
+                                       Enabled: true,
+                               },
+                               {
+                                       Name:    "service-three",
+                                       Started: true,
+                                       Enabled: true,
+                               },
+                       },
+                       "modified 3 service(s):\n\x1b[36m\"service-one\"\x1b[0m: started\n\x1b[36m\"service-two\"\x1b[0m: enabled\n\x1b[36m\"service-three\"\x1b[0m: started, enabled\n",
+               },
+
+               {
+                       "dry-run",
+                       true,
+                       false,
+                       []safcm.ServiceChange{
+                               {
+                                       Name:    "service-one",
+                                       Started: true,
+                               },
+                               {
+                                       Name:    "service-two",
+                                       Enabled: true,
+                               },
+                               {
+                                       Name:    "service-three",
+                                       Started: true,
+                                       Enabled: true,
+                               },
+                       },
+                       `will modify 3 service(s): (dry-run)
+"service-one": started
+"service-two": enabled
+"service-three": started, enabled
+`,
+               },
+
+               {
+                       "dry-run (tty)",
+                       true,
+                       true,
+                       []safcm.ServiceChange{
+                               {
+                                       Name:    "service-one",
+                                       Started: true,
+                               },
+                               {
+                                       Name:    "service-two",
+                                       Enabled: true,
+                               },
+                               {
+                                       Name:    "service-three",
+                                       Started: true,
+                                       Enabled: true,
+                               },
+                       },
+                       "will modify 3 service(s): (dry-run)\n\x1b[36m\"service-one\"\x1b[0m: started\n\x1b[36m\"service-two\"\x1b[0m: enabled\n\x1b[36m\"service-three\"\x1b[0m: started, enabled\n",
+               },
+
+               {
+                       "escaping",
+                       false,
+                       false,
+                       []safcm.ServiceChange{
+                               {
+                                       Name: "\x00",
+                               },
+                               {
+                                       Name:    "\x01",
+                                       Started: true,
+                                       Enabled: true,
+                               },
+                       },
+                       `modified 2 service(s):
+"\x00": 
+"\x01": started, enabled
+`,
+               },
+
+               {
+                       "escaping (tty)",
+                       false,
+                       true,
+                       []safcm.ServiceChange{
+                               {
+                                       Name: "\x00",
+                               },
+                               {
+                                       Name:    "\x01",
+                                       Started: true,
+                                       Enabled: true,
+                               },
+                       },
+                       "modified 2 service(s):\n\x1b[36m\"\\x00\"\x1b[0m: \n\x1b[36m\"\\x01\"\x1b[0m: started, enabled\n",
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       c := Changes{
+                               DryRun: tc.dryRun,
+                               IsTTY:  tc.isTTY,
+                       }
+
+                       res := c.FormatServiceChanges(tc.changes)
+                       testutil.AssertEqual(t, "res", res, tc.exp)
+               })
+       }
+}
+
+func TestFormatCommandChanges(t *testing.T) {
+       tests := []struct {
+               name    string
+               dryRun  bool
+               quiet   bool
+               isTTY   bool
+               changes []safcm.CommandChange
+               exp     string
+       }{
+
+               {
+                       "regular",
+                       false,
+                       false,
+                       false,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "fake command",
+                                       Output:  "fake output",
+                               },
+                               {
+                                       Command: "fake command with no output",
+                               },
+                               {
+                                       Command: "fake command with newline",
+                                       Output:  "fake output\n",
+                               },
+                               {
+                                       Command: "fake command with more output",
+                                       Output:  "fake out\nfake put\nfake\n",
+                               },
+                               {
+                                       Command: "fake failed command",
+                                       Output:  "fake output",
+                                       Error:   "fake error",
+                               },
+                       },
+                       `executed 5 command(s):
+"fake command":
+   > fake output
+   > \ No newline at end of file
+"fake command with no output"
+"fake command with newline":
+   > fake output
+"fake command with more output":
+   > fake out
+   > fake put
+   > fake
+"fake failed command", failed: "fake error":
+   > fake output
+   > \ No newline at end of file
+`,
+               },
+
+               {
+                       "regular (tty)",
+                       false,
+                       false,
+                       true,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "fake command",
+                                       Output:  "fake output",
+                               },
+                               {
+                                       Command: "fake command with no output",
+                               },
+                               {
+                                       Command: "fake command with newline",
+                                       Output:  "fake output\n",
+                               },
+                               {
+                                       Command: "fake command with more output",
+                                       Output:  "fake out\nfake put\nfake\n",
+                               },
+                               {
+                                       Command: "fake failed command",
+                                       Output:  "fake output",
+                                       Error:   "fake error",
+                               },
+                       },
+                       "executed 5 command(s):\n\x1b[36m\"fake command\"\x1b[0m:\n   > fake output\n   > \\ No newline at end of file\n\x1b[36m\"fake command with no output\"\x1b[0m\n\x1b[36m\"fake command with newline\"\x1b[0m:\n   > fake output\n\x1b[36m\"fake command with more output\"\x1b[0m:\n   > fake out\n   > fake put\n   > fake\n\x1b[36m\"fake failed command\"\x1b[0m, failed: \"fake error\":\n   > fake output\n   > \\ No newline at end of file\n",
+               },
+
+               {
+                       "dry-run",
+                       true,
+                       false,
+                       false,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "fake command",
+                               },
+                       },
+                       `will execute 1 command(s): (dry-run)
+"fake command"
+`,
+               },
+
+               {
+                       "dry-run (tty)",
+                       true,
+                       false,
+                       true,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "fake command",
+                               },
+                       },
+                       "will execute 1 command(s): (dry-run)\n\x1b[36m\"fake command\"\x1b[0m\n",
+               },
+
+               {
+                       "quiet",
+                       false,
+                       true,
+                       false,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "fake command",
+                                       Output:  "fake output",
+                               },
+                               {
+                                       Command: "fake command with no output",
+                               },
+                               {
+                                       Command: "fake command with newline",
+                                       Output:  "fake output\n",
+                               },
+                               {
+                                       Command: "fake command with more output",
+                                       Output:  "fake out\nfake put\nfake\n",
+                               },
+                               {
+                                       Command: "fake failed command",
+                                       Output:  "fake output",
+                                       Error:   "fake error",
+                               },
+                       },
+                       `executed 5 command(s), 1 with no output (hidden):
+"fake command":
+   > fake output
+   > \ No newline at end of file
+"fake command with newline":
+   > fake output
+"fake command with more output":
+   > fake out
+   > fake put
+   > fake
+"fake failed command", failed: "fake error":
+   > fake output
+   > \ No newline at end of file
+`,
+               },
+
+               {
+                       "quiet (tty)",
+                       false,
+                       true,
+                       true,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "fake command",
+                                       Output:  "fake output",
+                               },
+                               {
+                                       Command: "fake command with no output",
+                               },
+                               {
+                                       Command: "fake command with newline",
+                                       Output:  "fake output\n",
+                               },
+                               {
+                                       Command: "fake command with more output",
+                                       Output:  "fake out\nfake put\nfake\n",
+                               },
+                               {
+                                       Command: "fake failed command",
+                                       Output:  "fake output",
+                                       Error:   "fake error",
+                               },
+                       },
+                       "executed 5 command(s), 1 with no output (hidden):\n\x1b[36m\"fake command\"\x1b[0m:\n   > fake output\n   > \\ No newline at end of file\n\x1b[36m\"fake command with newline\"\x1b[0m:\n   > fake output\n\x1b[36m\"fake command with more output\"\x1b[0m:\n   > fake out\n   > fake put\n   > fake\n\x1b[36m\"fake failed command\"\x1b[0m, failed: \"fake error\":\n   > fake output\n   > \\ No newline at end of file\n",
+               },
+
+               {
+                       "quiet (only quiet commands)",
+                       false,
+                       true,
+                       false,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "fake command with no output",
+                               },
+                               {
+                                       Command: "fake command with no output",
+                               },
+                       },
+                       `executed 2 command(s), 2 with no output (hidden)
+`,
+               },
+
+               {
+                       "quiet (quiet with errors)",
+                       false,
+                       true,
+                       false,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "fake command with no output but error",
+                                       Error:   "fake error",
+                               },
+                               {
+                                       Command: "fake command with no output",
+                               },
+                       },
+                       `executed 2 command(s), 1 with no output (hidden):
+"fake command with no output but error", failed: "fake error"
+`,
+               },
+
+               {
+                       "quiet & dry-run",
+                       true,
+                       true,
+                       false,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "fake command",
+                               },
+                               {
+                                       Command: "fake command with no output",
+                               },
+                               {
+                                       Command: "fake command with newline",
+                               },
+                               {
+                                       Command: "fake command with more output",
+                               },
+                               {
+                                       Command: "fake failed command",
+                               },
+                       },
+                       `will execute 5 command(s) (dry-run)
+`,
+               },
+
+               {
+                       "escaping",
+                       false,
+                       false,
+                       false,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "\x00",
+                                       Trigger: "\x01",
+                                       Output:  "\x02",
+                                       Error:   "\x03",
+                               },
+                       },
+                       `executed 1 command(s):
+"\x00", trigger for "\x01", failed: "\x03":
+   > \x02
+   > \ No newline at end of file
+`,
+               },
+
+               {
+                       "escaping (tty)",
+                       false,
+                       false,
+                       true,
+                       []safcm.CommandChange{
+                               {
+                                       Command: "\x00",
+                                       Trigger: "\x01",
+                                       Output:  "\x02",
+                                       Error:   "\x03",
+                               },
+                       },
+                       "executed 1 command(s):\n\x1b[36m\"\\x00\"\x1b[0m, trigger for \"\\x01\", failed: \"\\x03\":\n   > \x1b[35m\\x02\x1b[0m\n   > \\ No newline at end of file\n",
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       c := Changes{
+                               DryRun: tc.dryRun,
+                               Quiet:  tc.quiet,
+                               IsTTY:  tc.isTTY,
+                       }
+
+                       res := c.FormatCommandChanges(tc.changes)
+                       testutil.AssertEqual(t, "res", res, tc.exp)
+               })
+       }
+}