From 530aeb23fb9b1f8fb633515d4cf6bf7b1c543d27 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Mon, 12 Aug 2024 15:43:06 +0200 Subject: [PATCH 01/44] Add flavor config parameter --- src/config/param_commit_failures.go | 2 +- src/config/param_flavor.go | 52 ++++++++++++++++++++++++++ src/config/tcr_config.go | 4 ++ src/config/tcr_config_test.go | 1 + src/params/params.go | 1 + src/params/params_test_data_builder.go | 8 ++++ 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/config/param_flavor.go diff --git a/src/config/param_commit_failures.go b/src/config/param_commit_failures.go index 1cd7aa8c..f994960a 100644 --- a/src/config/param_commit_failures.go +++ b/src/config/param_commit_failures.go @@ -37,7 +37,7 @@ func AddCommitFailuresParam(cmd *cobra.Command) *BoolParam { }, cobraSettings: cobraSettings{ name: "commit-failures", - shorthand: "f", + shorthand: "F", usage: "enable committing reverts on tests failure", persistent: true, }, diff --git a/src/config/param_flavor.go b/src/config/param_flavor.go new file mode 100644 index 00000000..19c24133 --- /dev/null +++ b/src/config/param_flavor.go @@ -0,0 +1,52 @@ +/* +Copyright (c) 2024 Murex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package config + +import ( + "github.com/spf13/cobra" +) + +// AddFlavorParam adds flavor parameter to the provided command +func AddFlavorParam(cmd *cobra.Command) *StringParam { + param := StringParam{ + s: paramSettings{ + viperSettings: viperSettings{ + enabled: true, + keyPath: "config.tcr", + name: "flavor", + }, + cobraSettings: cobraSettings{ + name: "flavor", + shorthand: "f", + usage: "indicate the flavor to be used by TCR: nice (default) or original", + persistent: true, + }, + }, + v: paramValueString{ + value: "", + defaultValue: "nice", + }, + } + param.addToCommand(cmd) + return ¶m +} diff --git a/src/config/tcr_config.go b/src/config/tcr_config.go index 102502e6..d45d57ae 100644 --- a/src/config/tcr_config.go +++ b/src/config/tcr_config.go @@ -46,6 +46,7 @@ type TcrConfig struct { PollingPeriod *DurationParam MobTimerDuration *DurationParam AutoPush *BoolParam + Flavor *StringParam CommitFailures *BoolParam VCS *StringParam MessageSuffix *StringParam @@ -59,6 +60,7 @@ func (c TcrConfig) reset() { c.PollingPeriod.reset() c.MobTimerDuration.reset() c.AutoPush.reset() + c.Flavor.reset() c.CommitFailures.reset() c.VCS.reset() c.MessageSuffix.reset() @@ -200,6 +202,7 @@ func AddParameters(cmd *cobra.Command, defaultDir string) { Config.PollingPeriod = AddPollingPeriodParam(cmd) Config.MobTimerDuration = AddMobTimerDurationParam(cmd) Config.AutoPush = AddAutoPushParam(cmd) + Config.Flavor = AddFlavorParam(cmd) Config.CommitFailures = AddCommitFailuresParam(cmd) Config.VCS = AddVCSParam(cmd) Config.MessageSuffix = AddMessageSuffixParam(cmd) @@ -217,6 +220,7 @@ func UpdateEngineParams(p *params.Params) { p.Toolchain = Config.Toolchain.GetValue() p.PollingPeriod = Config.PollingPeriod.GetValue() p.AutoPush = Config.AutoPush.GetValue() + p.Flavor = Config.Flavor.GetValue() p.CommitFailures = Config.CommitFailures.GetValue() p.VCS = Config.VCS.GetValue() p.MessageSuffix = Config.MessageSuffix.GetValue() diff --git a/src/config/tcr_config_test.go b/src/config/tcr_config_test.go index 07dad702..69c690d1 100644 --- a/src/config/tcr_config_test.go +++ b/src/config/tcr_config_test.go @@ -241,6 +241,7 @@ func Test_show_tcr_config_with_default_values(t *testing.T) { fmt.Sprintf("%v.git.commit-failures: %v", prefix, false), fmt.Sprintf("%v.git.polling-period: %v", prefix, 2*time.Second), fmt.Sprintf("%v.mob-timer.duration: %v", prefix, 5*time.Minute), + fmt.Sprintf("%v.tcr.flavor: %v", prefix, "nice"), fmt.Sprintf("%v.tcr.language: %v", prefix, ""), fmt.Sprintf("%v.tcr.toolchain: %v", prefix, ""), fmt.Sprintf("%v.tcr.trace: %v", prefix, "none"), diff --git a/src/params/params.go b/src/params/params.go index 1b68fe0b..58f644e8 100644 --- a/src/params/params.go +++ b/src/params/params.go @@ -36,6 +36,7 @@ type Params struct { Toolchain string MobTurnDuration time.Duration AutoPush bool + Flavor string CommitFailures bool PollingPeriod time.Duration Mode runmode.RunMode diff --git a/src/params/params_test_data_builder.go b/src/params/params_test_data_builder.go index c60772cc..0edb5c70 100644 --- a/src/params/params_test_data_builder.go +++ b/src/params/params_test_data_builder.go @@ -39,6 +39,7 @@ func AParamSet(builders ...func(params *Params)) *Params { Toolchain: "", MobTurnDuration: 0, AutoPush: false, + Flavor: "", PollingPeriod: 0, Mode: runmode.OneShot{}, VCS: "git", @@ -107,6 +108,13 @@ func WithAutoPush(value bool) func(params *Params) { } } +// WithFlavor sets the provided value as the flavor to be used +func WithFlavor(flavor string) func(params *Params) { + return func(params *Params) { + params.Flavor = flavor + } +} + // WithCommitFailures sets commit-failures flag to the provided value func WithCommitFailures(value bool) func(params *Params) { return func(params *Params) { From f4c70572377baab9e6af565cda41af2a0bf0e603 Mon Sep 17 00:00:00 2001 From: Philippe Bourgau Date: Tue, 13 Aug 2024 11:25:32 +0200 Subject: [PATCH 02/44] [#674] Add Flavor string field to TCREngine - add a test for SessionInfo - add a TCR.SetFlavor(...) method to interface --- src/engine/session_info.go | 1 + src/engine/tcr.go | 7 +++++++ src/engine/tcr_test.go | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/src/engine/session_info.go b/src/engine/session_info.go index d150af70..552b33e7 100644 --- a/src/engine/session_info.go +++ b/src/engine/session_info.go @@ -31,6 +31,7 @@ type SessionInfo struct { VCSName string VCSSessionSummary string CommitOnFail bool + Flavor string GitAutoPush bool MessageSuffix string } diff --git a/src/engine/tcr.go b/src/engine/tcr.go index ceace219..cbce48bb 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -58,6 +58,7 @@ type ( ToggleAutoPush() SetAutoPush(flag bool) SetCommitOnFail(flag bool) + SetFlavor(flavor string) GetCurrentRole() role.Role RunAsDriver() RunAsNavigator() @@ -92,6 +93,7 @@ type ( // before starting a new one roleMutex sync.Mutex commitOnFail bool + flavor string messageSuffix string // shoot channel is used for handling interruptions coming from the UI shoot chan bool @@ -106,6 +108,10 @@ type ( } ) +func (tcr *TCREngine) SetFlavor(flavor string) { + tcr.flavor = flavor +} + const traceReporterWaitingTime = 100 * time.Millisecond const fsWatchRearmDelay = 100 * time.Millisecond @@ -678,6 +684,7 @@ func (tcr *TCREngine) GetSessionInfo() SessionInfo { VCSSessionSummary: tcr.vcs.SessionSummary(), GitAutoPush: tcr.vcs.IsAutoPushEnabled(), CommitOnFail: tcr.commitOnFail, + Flavor: tcr.flavor, MessageSuffix: tcr.messageSuffix, } } diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 97e2ad0c..76dec131 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -465,6 +465,24 @@ func Test_set_commit_on_fail(t *testing.T) { } } +func Test_set_flavor(t *testing.T) { + var tcr TCRInterface + testFlags := []struct { + desc string + flavor string + }{ + {"Nice Flavor", "nice"}, + {"Original Flavor", "original"}, + } + for _, tt := range testFlags { + t.Run(tt.desc, func(t *testing.T) { + tcr, _ = initTCREngineWithFakes(nil, nil, nil, nil) + tcr.SetFlavor(tt.flavor) + assert.Equal(t, tt.flavor, tcr.GetSessionInfo().Flavor) + }) + } +} + func Test_vcs_pull_calls_vcs_command(t *testing.T) { tcr, vcsFake := initTCREngineWithFakes(nil, nil, nil, nil) tcr.VCSPull() From b7c96a4ee361e567b6c07ab364c07373e4a31d65 Mon Sep 17 00:00:00 2001 From: Philippe Bourgau Date: Tue, 13 Aug 2024 11:45:17 +0200 Subject: [PATCH 03/44] [#674] Displaying the flavor in Terminal info - pass Flavor param to TCREngine - print the Flavor in Terminal.ShowSessionInfo - add "nice" flavor by default in fakes and test doubles --- src/cli/terminal_ui.go | 5 +++++ src/cli/terminal_ui_test.go | 3 ++- src/engine/tcr.go | 1 + src/engine/tcr_test_fake.go | 1 + src/params/params_test_data_builder.go | 2 +- 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cli/terminal_ui.go b/src/cli/terminal_ui.go index 7ed8ac23..1f153558 100644 --- a/src/cli/terminal_ui.go +++ b/src/cli/terminal_ui.go @@ -334,6 +334,7 @@ func (term *TerminalUI) ShowSessionInfo() { term.printInfo("Work Directory: ", info.WorkDir) term.printInfo("Language=", info.LanguageName, ", Toolchain=", info.ToolchainName) term.printVCSInfo(info) + term.printFlavor(info) term.printMessageSuffix(info.MessageSuffix) } @@ -352,6 +353,10 @@ func (term *TerminalUI) printVCSInfo(info engine.SessionInfo) { } } +func (term *TerminalUI) printFlavor(info engine.SessionInfo) { + term.printInfo("Flavor is ", info.Flavor) +} + func (term *TerminalUI) printMessageSuffix(suffix string) { if suffix == "" { return diff --git a/src/cli/terminal_ui_test.go b/src/cli/terminal_ui_test.go index 50f49261..d08f535d 100644 --- a/src/cli/terminal_ui_test.go +++ b/src/cli/terminal_ui_test.go @@ -511,7 +511,8 @@ func Test_show_session_info(t *testing.T) { expected := asCyanTraceWithSeparatorLine("Base Directory: fake") + asCyanTrace("Work Directory: fake") + asCyanTrace("Language=fake, Toolchain=fake") + - asYellowTrace("VCS \"fake\" is unknown") + asYellowTrace("VCS \"fake\" is unknown") + + asCyanTrace("Flavor is nice") assert.Equal(t, expected, capturer.CaptureStdout(func() { term, _, _ := terminalSetup(*params.AParamSet()) diff --git a/src/engine/tcr.go b/src/engine/tcr.go index cbce48bb..6180e145 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -181,6 +181,7 @@ func (tcr *TCREngine) Init(p params.Params) { tcr.vcs.EnableAutoPush(p.AutoPush) tcr.SetCommitOnFail(p.CommitFailures) + tcr.SetFlavor(p.Flavor) tcr.setMobTimerDuration(p.MobTurnDuration) tcr.ui.ShowRunningMode(tcr.mode) diff --git a/src/engine/tcr_test_fake.go b/src/engine/tcr_test_fake.go index aa0fac1d..58097168 100644 --- a/src/engine/tcr_test_fake.go +++ b/src/engine/tcr_test_fake.go @@ -79,6 +79,7 @@ func NewFakeTCREngine() *FakeTCREngine { VCSSessionSummary: "VCS session \"fake\"", GitAutoPush: false, CommitOnFail: false, + Flavor: "nice", }, } } diff --git a/src/params/params_test_data_builder.go b/src/params/params_test_data_builder.go index 0edb5c70..d202d5fe 100644 --- a/src/params/params_test_data_builder.go +++ b/src/params/params_test_data_builder.go @@ -39,7 +39,7 @@ func AParamSet(builders ...func(params *Params)) *Params { Toolchain: "", MobTurnDuration: 0, AutoPush: false, - Flavor: "", + Flavor: "nice", PollingPeriod: 0, Mode: runmode.OneShot{}, VCS: "git", From d9d22499c41218dc388f44b3bdb1e11a07f5857a Mon Sep 17 00:00:00 2001 From: Philippe Bourgau Date: Tue, 13 Aug 2024 12:05:01 +0200 Subject: [PATCH 04/44] [#674] Displaying the flavor in Web info - pass the Flavor through HTTP - update the html to display the flavor - adapt structs and tests --- src/http/api/session_info.go | 2 ++ src/http/api/session_info_test.go | 1 + .../components/tcr-session-info/tcr-session-info.component.html | 2 ++ .../tcr-session-info/tcr-session-info.component.spec.ts | 1 + webapp/src/app/interfaces/tcr-session-info.ts | 1 + webapp/src/app/services/tcr-session-info.service.spec.ts | 1 + 6 files changed, 8 insertions(+) diff --git a/src/http/api/session_info.go b/src/http/api/session_info.go index 452b52e8..1a2a2cbd 100644 --- a/src/http/api/session_info.go +++ b/src/http/api/session_info.go @@ -35,6 +35,7 @@ type sessionInfo struct { VCSName string `json:"vcsName"` VCSSessionSummary string `json:"vcsSession"` CommitOnFail bool `json:"commitOnFail"` + Flavor string `json:"flavor"` GitAutoPush bool `json:"gitAutoPush"` MessageSuffix string `json:"messageSuffix"` } @@ -51,6 +52,7 @@ func SessionInfoGetHandler(c *gin.Context) { VCSName: info.VCSName, VCSSessionSummary: info.VCSSessionSummary, CommitOnFail: info.CommitOnFail, + Flavor: info.Flavor, GitAutoPush: info.GitAutoPush, MessageSuffix: info.MessageSuffix, } diff --git a/src/http/api/session_info_test.go b/src/http/api/session_info_test.go index 00d49641..229baf1b 100644 --- a/src/http/api/session_info_test.go +++ b/src/http/api/session_info_test.go @@ -57,6 +57,7 @@ func Test_session_info_get_handler(t *testing.T) { VCSName: info.VCSName, VCSSessionSummary: info.VCSSessionSummary, CommitOnFail: info.CommitOnFail, + Flavor: info.Flavor, GitAutoPush: info.GitAutoPush, MessageSuffix: info.MessageSuffix, } diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html index 70f1e153..6035cf00 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html @@ -76,6 +76,8 @@

VCS Commit Mes

VCS Commit-On-Fail {{ sessionInfo.commitOnFail | onOff }}

+

+ Flavor {{ sessionInfo.flavor | showEmpty }}

Git Auto-Push {{ sessionInfo.gitAutoPush | onOff }}

diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts index 887ad5ad..1ca059c3 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts @@ -29,6 +29,7 @@ import {TcrSessionInfoComponent} from "./tcr-session-info.component"; const sample: TcrSessionInfo = { baseDir: "/my/base/dir", commitOnFail: false, + flavor: "nice", gitAutoPush: false, language: "java", messageSuffix: "my-suffix", diff --git a/webapp/src/app/interfaces/tcr-session-info.ts b/webapp/src/app/interfaces/tcr-session-info.ts index 0366831f..306a174f 100644 --- a/webapp/src/app/interfaces/tcr-session-info.ts +++ b/webapp/src/app/interfaces/tcr-session-info.ts @@ -28,6 +28,7 @@ export interface TcrSessionInfo { vcsName: string; vcsSession: string; commitOnFail: boolean; + flavor: string; gitAutoPush: boolean; messageSuffix: string; } diff --git a/webapp/src/app/services/tcr-session-info.service.spec.ts b/webapp/src/app/services/tcr-session-info.service.spec.ts index bd9a674a..a511f26d 100644 --- a/webapp/src/app/services/tcr-session-info.service.spec.ts +++ b/webapp/src/app/services/tcr-session-info.service.spec.ts @@ -58,6 +58,7 @@ describe('TcrSessionInfoService', () => { const sample: TcrSessionInfo = { baseDir: "/my/base/dir", commitOnFail: false, + flavor: "nice", gitAutoPush: false, language: "java", messageSuffix: "my-suffix", From 2f0c90e64a27b2dfd3b7d55da7d76c0870062354 Mon Sep 17 00:00:00 2001 From: Philippe Bourgau Date: Tue, 13 Aug 2024 12:05:58 +0200 Subject: [PATCH 05/44] [#674] Update documentation with new Flavor parameters --- doc/tcr.md | 3 ++- doc/tcr_check.md | 3 ++- doc/tcr_config.md | 3 ++- doc/tcr_config_reset.md | 3 ++- doc/tcr_config_save.md | 3 ++- doc/tcr_config_show.md | 3 ++- doc/tcr_info.md | 3 ++- doc/tcr_log.md | 3 ++- doc/tcr_mob.md | 3 ++- doc/tcr_one-shot.md | 3 ++- doc/tcr_solo.md | 3 ++- doc/tcr_stats.md | 3 ++- doc/tcr_web.md | 3 ++- 13 files changed, 26 insertions(+), 13 deletions(-) diff --git a/doc/tcr.md b/doc/tcr.md index 5e1c62c8..cfc5fbac 100644 --- a/doc/tcr.md +++ b/doc/tcr.md @@ -18,9 +18,10 @@ tcr [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -h, --help help for tcr -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") diff --git a/doc/tcr_check.md b/doc/tcr_check.md index 33bd71d4..34608d47 100644 --- a/doc/tcr_check.md +++ b/doc/tcr_check.md @@ -46,9 +46,10 @@ tcr check [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_config.md b/doc/tcr_config.md index 5479e989..bc4431da 100644 --- a/doc/tcr_config.md +++ b/doc/tcr_config.md @@ -24,9 +24,10 @@ tcr config [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_config_reset.md b/doc/tcr_config_reset.md index 9c219fe9..c55891b0 100644 --- a/doc/tcr_config_reset.md +++ b/doc/tcr_config_reset.md @@ -24,9 +24,10 @@ tcr config reset [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_config_save.md b/doc/tcr_config_save.md index c19a0090..35ad6b25 100644 --- a/doc/tcr_config_save.md +++ b/doc/tcr_config_save.md @@ -24,9 +24,10 @@ tcr config save [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_config_show.md b/doc/tcr_config_show.md index 8e39c0bf..d6a9c53a 100644 --- a/doc/tcr_config_show.md +++ b/doc/tcr_config_show.md @@ -24,9 +24,10 @@ tcr config show [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_info.md b/doc/tcr_info.md index 59aae2b3..ea5b20e8 100644 --- a/doc/tcr_info.md +++ b/doc/tcr_info.md @@ -24,9 +24,10 @@ tcr info [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_log.md b/doc/tcr_log.md index b380a16c..4282eb23 100644 --- a/doc/tcr_log.md +++ b/doc/tcr_log.md @@ -32,9 +32,10 @@ tcr log [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_mob.md b/doc/tcr_mob.md index 14f49c1f..e2677144 100644 --- a/doc/tcr_mob.md +++ b/doc/tcr_mob.md @@ -24,9 +24,10 @@ tcr mob [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_one-shot.md b/doc/tcr_one-shot.md index f8f00997..526993d0 100644 --- a/doc/tcr_one-shot.md +++ b/doc/tcr_one-shot.md @@ -35,9 +35,10 @@ tcr one-shot [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_solo.md b/doc/tcr_solo.md index c927ebb7..2aecc9dc 100644 --- a/doc/tcr_solo.md +++ b/doc/tcr_solo.md @@ -24,9 +24,10 @@ tcr solo [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_stats.md b/doc/tcr_stats.md index 9542abfb..da27b4da 100644 --- a/doc/tcr_stats.md +++ b/doc/tcr_stats.md @@ -52,9 +52,10 @@ tcr stats [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator diff --git a/doc/tcr_web.md b/doc/tcr_web.md index fb43a0f8..5c6c30f9 100644 --- a/doc/tcr_web.md +++ b/doc/tcr_web.md @@ -30,9 +30,10 @@ tcr web [flags] ``` -p, --auto-push enable VCS push after every commit -b, --base-dir string indicate the directory from which TCR is looking for files (default: current directory) - -f, --commit-failures enable committing reverts on tests failure + -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer + -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator From aa528d3d8c1142a2ce43f2138531269e7f4b49be Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Tue, 13 Aug 2024 13:12:04 +0200 Subject: [PATCH 06/44] [#674] Fix test case on tcr.GetSessionInfo() --- src/engine/tcr_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 76dec131..ff3388b2 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -366,6 +366,7 @@ func initTCREngineWithFakes( params.WithMobTimerDuration(p.MobTurnDuration), params.WithAutoPush(p.AutoPush), params.WithCommitFailures(p.CommitFailures), + params.WithFlavor(p.Flavor), params.WithPollingPeriod(p.PollingPeriod), params.WithRunMode(p.Mode), params.WithVCS(p.VCS), @@ -535,6 +536,7 @@ func Test_get_session_info(t *testing.T) { ToolchainName: "fake-toolchain", VCSName: fake.Name, VCSSessionSummary: "VCS session \"" + fake.Name + "\"", + Flavor: "nice", GitAutoPush: false, } assert.Equal(t, expected, tcr.GetSessionInfo()) From b7b31d5c7c7f635a556270c104e2537ce024f4ab Mon Sep 17 00:00:00 2001 From: Ahmad ATWI Date: Tue, 13 Aug 2024 14:36:20 +0200 Subject: [PATCH 07/44] [#674] Improve the wording from flavor to variant --- .../components/tcr-session-info/tcr-session-info.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html index 6035cf00..eb091f72 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html @@ -77,7 +77,7 @@

VCS Commit Mes

VCS Commit-On-Fail {{ sessionInfo.commitOnFail | onOff }}

- Flavor {{ sessionInfo.flavor | showEmpty }}

+ Variant "{{ sessionInfo.flavor | showEmpty }}"

Git Auto-Push {{ sessionInfo.gitAutoPush | onOff }}

From 4b07dff047035e5e3df5108ae471f18b2d9efb4c Mon Sep 17 00:00:00 2001 From: Ahmad ATWI Date: Tue, 13 Aug 2024 14:54:04 +0200 Subject: [PATCH 08/44] [#674] Rename Flavor to Variant - Big grep replace - Update docs --- doc/tcr.md | 2 +- doc/tcr_check.md | 2 +- doc/tcr_config.md | 2 +- doc/tcr_config_reset.md | 2 +- doc/tcr_config_save.md | 2 +- doc/tcr_config_show.md | 2 +- doc/tcr_info.md | 2 +- doc/tcr_log.md | 2 +- doc/tcr_mob.md | 2 +- doc/tcr_one-shot.md | 2 +- doc/tcr_solo.md | 2 +- doc/tcr_stats.md | 2 +- doc/tcr_web.md | 2 +- src/cli/terminal_ui.go | 6 +++--- src/cli/terminal_ui_test.go | 2 +- .../{param_flavor.go => param_variant.go} | 12 ++++++------ src/config/tcr_config.go | 8 ++++---- src/config/tcr_config_test.go | 2 +- src/engine/session_info.go | 2 +- src/engine/tcr.go | 12 ++++++------ src/engine/tcr_test.go | 18 +++++++++--------- src/engine/tcr_test_fake.go | 2 +- src/http/api/session_info.go | 4 ++-- src/http/api/session_info_test.go | 2 +- src/params/params.go | 2 +- src/params/params_test_data_builder.go | 8 ++++---- .../tcr-session-info.component.html | 2 +- .../tcr-session-info.component.spec.ts | 2 +- webapp/src/app/interfaces/tcr-session-info.ts | 2 +- .../services/tcr-session-info.service.spec.ts | 2 +- 30 files changed, 57 insertions(+), 57 deletions(-) rename src/config/{param_flavor.go => param_variant.go} (83%) diff --git a/doc/tcr.md b/doc/tcr.md index cfc5fbac..0426bac1 100644 --- a/doc/tcr.md +++ b/doc/tcr.md @@ -21,7 +21,6 @@ tcr [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -h, --help help for tcr -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") @@ -29,6 +28,7 @@ tcr [flags] -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_check.md b/doc/tcr_check.md index 34608d47..05e6a56f 100644 --- a/doc/tcr_check.md +++ b/doc/tcr_check.md @@ -49,13 +49,13 @@ tcr check [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_config.md b/doc/tcr_config.md index bc4431da..d19fc509 100644 --- a/doc/tcr_config.md +++ b/doc/tcr_config.md @@ -27,13 +27,13 @@ tcr config [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_config_reset.md b/doc/tcr_config_reset.md index c55891b0..b0e977a8 100644 --- a/doc/tcr_config_reset.md +++ b/doc/tcr_config_reset.md @@ -27,13 +27,13 @@ tcr config reset [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_config_save.md b/doc/tcr_config_save.md index 35ad6b25..212ac416 100644 --- a/doc/tcr_config_save.md +++ b/doc/tcr_config_save.md @@ -27,13 +27,13 @@ tcr config save [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_config_show.md b/doc/tcr_config_show.md index d6a9c53a..d86b9f3d 100644 --- a/doc/tcr_config_show.md +++ b/doc/tcr_config_show.md @@ -27,13 +27,13 @@ tcr config show [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_info.md b/doc/tcr_info.md index ea5b20e8..a9f7da92 100644 --- a/doc/tcr_info.md +++ b/doc/tcr_info.md @@ -27,13 +27,13 @@ tcr info [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_log.md b/doc/tcr_log.md index 4282eb23..e38c9597 100644 --- a/doc/tcr_log.md +++ b/doc/tcr_log.md @@ -35,13 +35,13 @@ tcr log [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_mob.md b/doc/tcr_mob.md index e2677144..fb263a56 100644 --- a/doc/tcr_mob.md +++ b/doc/tcr_mob.md @@ -27,13 +27,13 @@ tcr mob [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_one-shot.md b/doc/tcr_one-shot.md index 526993d0..24cd6dbc 100644 --- a/doc/tcr_one-shot.md +++ b/doc/tcr_one-shot.md @@ -38,13 +38,13 @@ tcr one-shot [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_solo.md b/doc/tcr_solo.md index 2aecc9dc..8d0c52d5 100644 --- a/doc/tcr_solo.md +++ b/doc/tcr_solo.md @@ -27,13 +27,13 @@ tcr solo [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_stats.md b/doc/tcr_stats.md index da27b4da..997ea944 100644 --- a/doc/tcr_stats.md +++ b/doc/tcr_stats.md @@ -55,13 +55,13 @@ tcr stats [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/doc/tcr_web.md b/doc/tcr_web.md index 5c6c30f9..76da34cd 100644 --- a/doc/tcr_web.md +++ b/doc/tcr_web.md @@ -33,13 +33,13 @@ tcr web [flags] -F, --commit-failures enable committing reverts on tests failure -c, --config-dir string indicate the directory where TCR configuration is stored (default: current directory) -d, --duration duration set the duration for role rotation countdown timer - -f, --flavor string indicate the flavor to be used by TCR: nice (default) or original -l, --language string indicate the programming language to be used by TCR -m, --message-suffix string indicate text to append at the end of TCR commit messages (ex: "[#1234]") -o, --polling duration set VCS polling period when running as navigator -P, --port-number int indicate port number used by TCR HTTP server in web mode (experimental) (default: 8483) -t, --toolchain string indicate the toolchain to be used by TCR -T, --trace string indicate trace options. Recognized values: none (default), vcs or http + -r, --variant string indicate the variant to be used by TCR: nice (default) or original -V, --vcs string indicate the VCS (version control system) to be used by TCR: git (default) or p4 -w, --work-dir string indicate the directory from which TCR is running (default: current directory) ``` diff --git a/src/cli/terminal_ui.go b/src/cli/terminal_ui.go index 1f153558..22a428a5 100644 --- a/src/cli/terminal_ui.go +++ b/src/cli/terminal_ui.go @@ -334,7 +334,7 @@ func (term *TerminalUI) ShowSessionInfo() { term.printInfo("Work Directory: ", info.WorkDir) term.printInfo("Language=", info.LanguageName, ", Toolchain=", info.ToolchainName) term.printVCSInfo(info) - term.printFlavor(info) + term.printVariant(info) term.printMessageSuffix(info.MessageSuffix) } @@ -353,8 +353,8 @@ func (term *TerminalUI) printVCSInfo(info engine.SessionInfo) { } } -func (term *TerminalUI) printFlavor(info engine.SessionInfo) { - term.printInfo("Flavor is ", info.Flavor) +func (term *TerminalUI) printVariant(info engine.SessionInfo) { + term.printInfo("Variant is ", info.Variant) } func (term *TerminalUI) printMessageSuffix(suffix string) { diff --git a/src/cli/terminal_ui_test.go b/src/cli/terminal_ui_test.go index d08f535d..2d933a51 100644 --- a/src/cli/terminal_ui_test.go +++ b/src/cli/terminal_ui_test.go @@ -512,7 +512,7 @@ func Test_show_session_info(t *testing.T) { asCyanTrace("Work Directory: fake") + asCyanTrace("Language=fake, Toolchain=fake") + asYellowTrace("VCS \"fake\" is unknown") + - asCyanTrace("Flavor is nice") + asCyanTrace("Variant is nice") assert.Equal(t, expected, capturer.CaptureStdout(func() { term, _, _ := terminalSetup(*params.AParamSet()) diff --git a/src/config/param_flavor.go b/src/config/param_variant.go similarity index 83% rename from src/config/param_flavor.go rename to src/config/param_variant.go index 19c24133..2d85eeae 100644 --- a/src/config/param_flavor.go +++ b/src/config/param_variant.go @@ -26,19 +26,19 @@ import ( "github.com/spf13/cobra" ) -// AddFlavorParam adds flavor parameter to the provided command -func AddFlavorParam(cmd *cobra.Command) *StringParam { +// AddVariantParam adds variant parameter to the provided command +func AddVariantParam(cmd *cobra.Command) *StringParam { param := StringParam{ s: paramSettings{ viperSettings: viperSettings{ enabled: true, keyPath: "config.tcr", - name: "flavor", + name: "variant", }, cobraSettings: cobraSettings{ - name: "flavor", - shorthand: "f", - usage: "indicate the flavor to be used by TCR: nice (default) or original", + name: "variant", + shorthand: "r", + usage: "indicate the variant to be used by TCR: nice (default) or original", persistent: true, }, }, diff --git a/src/config/tcr_config.go b/src/config/tcr_config.go index d45d57ae..185eb4d2 100644 --- a/src/config/tcr_config.go +++ b/src/config/tcr_config.go @@ -46,7 +46,7 @@ type TcrConfig struct { PollingPeriod *DurationParam MobTimerDuration *DurationParam AutoPush *BoolParam - Flavor *StringParam + Variant *StringParam CommitFailures *BoolParam VCS *StringParam MessageSuffix *StringParam @@ -60,7 +60,7 @@ func (c TcrConfig) reset() { c.PollingPeriod.reset() c.MobTimerDuration.reset() c.AutoPush.reset() - c.Flavor.reset() + c.Variant.reset() c.CommitFailures.reset() c.VCS.reset() c.MessageSuffix.reset() @@ -202,7 +202,7 @@ func AddParameters(cmd *cobra.Command, defaultDir string) { Config.PollingPeriod = AddPollingPeriodParam(cmd) Config.MobTimerDuration = AddMobTimerDurationParam(cmd) Config.AutoPush = AddAutoPushParam(cmd) - Config.Flavor = AddFlavorParam(cmd) + Config.Variant = AddVariantParam(cmd) Config.CommitFailures = AddCommitFailuresParam(cmd) Config.VCS = AddVCSParam(cmd) Config.MessageSuffix = AddMessageSuffixParam(cmd) @@ -220,7 +220,7 @@ func UpdateEngineParams(p *params.Params) { p.Toolchain = Config.Toolchain.GetValue() p.PollingPeriod = Config.PollingPeriod.GetValue() p.AutoPush = Config.AutoPush.GetValue() - p.Flavor = Config.Flavor.GetValue() + p.Variant = Config.Variant.GetValue() p.CommitFailures = Config.CommitFailures.GetValue() p.VCS = Config.VCS.GetValue() p.MessageSuffix = Config.MessageSuffix.GetValue() diff --git a/src/config/tcr_config_test.go b/src/config/tcr_config_test.go index 69c690d1..8e75b23c 100644 --- a/src/config/tcr_config_test.go +++ b/src/config/tcr_config_test.go @@ -241,10 +241,10 @@ func Test_show_tcr_config_with_default_values(t *testing.T) { fmt.Sprintf("%v.git.commit-failures: %v", prefix, false), fmt.Sprintf("%v.git.polling-period: %v", prefix, 2*time.Second), fmt.Sprintf("%v.mob-timer.duration: %v", prefix, 5*time.Minute), - fmt.Sprintf("%v.tcr.flavor: %v", prefix, "nice"), fmt.Sprintf("%v.tcr.language: %v", prefix, ""), fmt.Sprintf("%v.tcr.toolchain: %v", prefix, ""), fmt.Sprintf("%v.tcr.trace: %v", prefix, "none"), + fmt.Sprintf("%v.tcr.variant: %v", prefix, "nice"), fmt.Sprintf("%v.vcs.name: %v", prefix, "git"), } utils.AssertSimpleTrace(t, expected, diff --git a/src/engine/session_info.go b/src/engine/session_info.go index 552b33e7..fcfa525d 100644 --- a/src/engine/session_info.go +++ b/src/engine/session_info.go @@ -31,7 +31,7 @@ type SessionInfo struct { VCSName string VCSSessionSummary string CommitOnFail bool - Flavor string + Variant string GitAutoPush bool MessageSuffix string } diff --git a/src/engine/tcr.go b/src/engine/tcr.go index 6180e145..aecba66b 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -58,7 +58,7 @@ type ( ToggleAutoPush() SetAutoPush(flag bool) SetCommitOnFail(flag bool) - SetFlavor(flavor string) + SetVariant(variant string) GetCurrentRole() role.Role RunAsDriver() RunAsNavigator() @@ -93,7 +93,7 @@ type ( // before starting a new one roleMutex sync.Mutex commitOnFail bool - flavor string + variant string messageSuffix string // shoot channel is used for handling interruptions coming from the UI shoot chan bool @@ -108,8 +108,8 @@ type ( } ) -func (tcr *TCREngine) SetFlavor(flavor string) { - tcr.flavor = flavor +func (tcr *TCREngine) SetVariant(variant string) { + tcr.variant = variant } const traceReporterWaitingTime = 100 * time.Millisecond @@ -181,7 +181,7 @@ func (tcr *TCREngine) Init(p params.Params) { tcr.vcs.EnableAutoPush(p.AutoPush) tcr.SetCommitOnFail(p.CommitFailures) - tcr.SetFlavor(p.Flavor) + tcr.SetVariant(p.Variant) tcr.setMobTimerDuration(p.MobTurnDuration) tcr.ui.ShowRunningMode(tcr.mode) @@ -685,7 +685,7 @@ func (tcr *TCREngine) GetSessionInfo() SessionInfo { VCSSessionSummary: tcr.vcs.SessionSummary(), GitAutoPush: tcr.vcs.IsAutoPushEnabled(), CommitOnFail: tcr.commitOnFail, - Flavor: tcr.flavor, + Variant: tcr.variant, MessageSuffix: tcr.messageSuffix, } } diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index ff3388b2..795c101d 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -366,7 +366,7 @@ func initTCREngineWithFakes( params.WithMobTimerDuration(p.MobTurnDuration), params.WithAutoPush(p.AutoPush), params.WithCommitFailures(p.CommitFailures), - params.WithFlavor(p.Flavor), + params.WithVariant(p.Variant), params.WithPollingPeriod(p.PollingPeriod), params.WithRunMode(p.Mode), params.WithVCS(p.VCS), @@ -466,20 +466,20 @@ func Test_set_commit_on_fail(t *testing.T) { } } -func Test_set_flavor(t *testing.T) { +func Test_set_variant(t *testing.T) { var tcr TCRInterface testFlags := []struct { - desc string - flavor string + desc string + variant string }{ - {"Nice Flavor", "nice"}, - {"Original Flavor", "original"}, + {"Nice Variant", "nice"}, + {"Original Variant", "original"}, } for _, tt := range testFlags { t.Run(tt.desc, func(t *testing.T) { tcr, _ = initTCREngineWithFakes(nil, nil, nil, nil) - tcr.SetFlavor(tt.flavor) - assert.Equal(t, tt.flavor, tcr.GetSessionInfo().Flavor) + tcr.SetVariant(tt.variant) + assert.Equal(t, tt.variant, tcr.GetSessionInfo().Variant) }) } } @@ -536,7 +536,7 @@ func Test_get_session_info(t *testing.T) { ToolchainName: "fake-toolchain", VCSName: fake.Name, VCSSessionSummary: "VCS session \"" + fake.Name + "\"", - Flavor: "nice", + Variant: "nice", GitAutoPush: false, } assert.Equal(t, expected, tcr.GetSessionInfo()) diff --git a/src/engine/tcr_test_fake.go b/src/engine/tcr_test_fake.go index 58097168..d54cf275 100644 --- a/src/engine/tcr_test_fake.go +++ b/src/engine/tcr_test_fake.go @@ -79,7 +79,7 @@ func NewFakeTCREngine() *FakeTCREngine { VCSSessionSummary: "VCS session \"fake\"", GitAutoPush: false, CommitOnFail: false, - Flavor: "nice", + Variant: "nice", }, } } diff --git a/src/http/api/session_info.go b/src/http/api/session_info.go index 1a2a2cbd..2b782838 100644 --- a/src/http/api/session_info.go +++ b/src/http/api/session_info.go @@ -35,7 +35,7 @@ type sessionInfo struct { VCSName string `json:"vcsName"` VCSSessionSummary string `json:"vcsSession"` CommitOnFail bool `json:"commitOnFail"` - Flavor string `json:"flavor"` + Variant string `json:"variant"` GitAutoPush bool `json:"gitAutoPush"` MessageSuffix string `json:"messageSuffix"` } @@ -52,7 +52,7 @@ func SessionInfoGetHandler(c *gin.Context) { VCSName: info.VCSName, VCSSessionSummary: info.VCSSessionSummary, CommitOnFail: info.CommitOnFail, - Flavor: info.Flavor, + Variant: info.Variant, GitAutoPush: info.GitAutoPush, MessageSuffix: info.MessageSuffix, } diff --git a/src/http/api/session_info_test.go b/src/http/api/session_info_test.go index 229baf1b..1091a161 100644 --- a/src/http/api/session_info_test.go +++ b/src/http/api/session_info_test.go @@ -57,7 +57,7 @@ func Test_session_info_get_handler(t *testing.T) { VCSName: info.VCSName, VCSSessionSummary: info.VCSSessionSummary, CommitOnFail: info.CommitOnFail, - Flavor: info.Flavor, + Variant: info.Variant, GitAutoPush: info.GitAutoPush, MessageSuffix: info.MessageSuffix, } diff --git a/src/params/params.go b/src/params/params.go index 58f644e8..55b9f85d 100644 --- a/src/params/params.go +++ b/src/params/params.go @@ -36,7 +36,7 @@ type Params struct { Toolchain string MobTurnDuration time.Duration AutoPush bool - Flavor string + Variant string CommitFailures bool PollingPeriod time.Duration Mode runmode.RunMode diff --git a/src/params/params_test_data_builder.go b/src/params/params_test_data_builder.go index d202d5fe..47c12709 100644 --- a/src/params/params_test_data_builder.go +++ b/src/params/params_test_data_builder.go @@ -39,7 +39,7 @@ func AParamSet(builders ...func(params *Params)) *Params { Toolchain: "", MobTurnDuration: 0, AutoPush: false, - Flavor: "nice", + Variant: "nice", PollingPeriod: 0, Mode: runmode.OneShot{}, VCS: "git", @@ -108,10 +108,10 @@ func WithAutoPush(value bool) func(params *Params) { } } -// WithFlavor sets the provided value as the flavor to be used -func WithFlavor(flavor string) func(params *Params) { +// WithVariant sets the provided value as the variant to be used +func WithVariant(variant string) func(params *Params) { return func(params *Params) { - params.Flavor = flavor + params.Variant = variant } } diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html index eb091f72..3735d13c 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html @@ -77,7 +77,7 @@

VCS Commit Mes

VCS Commit-On-Fail {{ sessionInfo.commitOnFail | onOff }}

- Variant "{{ sessionInfo.flavor | showEmpty }}"

+ Variant "{{ sessionInfo.variant | showEmpty }}"

Git Auto-Push {{ sessionInfo.gitAutoPush | onOff }}

diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts index 1ca059c3..8fc7f196 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts @@ -29,7 +29,7 @@ import {TcrSessionInfoComponent} from "./tcr-session-info.component"; const sample: TcrSessionInfo = { baseDir: "/my/base/dir", commitOnFail: false, - flavor: "nice", + variant: "nice", gitAutoPush: false, language: "java", messageSuffix: "my-suffix", diff --git a/webapp/src/app/interfaces/tcr-session-info.ts b/webapp/src/app/interfaces/tcr-session-info.ts index 306a174f..4a3e9211 100644 --- a/webapp/src/app/interfaces/tcr-session-info.ts +++ b/webapp/src/app/interfaces/tcr-session-info.ts @@ -28,7 +28,7 @@ export interface TcrSessionInfo { vcsName: string; vcsSession: string; commitOnFail: boolean; - flavor: string; + variant: string; gitAutoPush: boolean; messageSuffix: string; } diff --git a/webapp/src/app/services/tcr-session-info.service.spec.ts b/webapp/src/app/services/tcr-session-info.service.spec.ts index a511f26d..c8a44cbb 100644 --- a/webapp/src/app/services/tcr-session-info.service.spec.ts +++ b/webapp/src/app/services/tcr-session-info.service.spec.ts @@ -58,7 +58,7 @@ describe('TcrSessionInfoService', () => { const sample: TcrSessionInfo = { baseDir: "/my/base/dir", commitOnFail: false, - flavor: "nice", + variant: "nice", gitAutoPush: false, language: "java", messageSuffix: "my-suffix", From 333df552157174e40102d2aba1ed4e4405cfe0ec Mon Sep 17 00:00:00 2001 From: Ahmad ATWI Date: Wed, 14 Aug 2024 10:26:03 +0200 Subject: [PATCH 09/44] [#674] Transform flavor from string to enum - Rename nice to relaxed - Rename original to btcr --- src/config/param_variant.go | 4 ++-- src/config/tcr_config_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/param_variant.go b/src/config/param_variant.go index 2d85eeae..5d320c67 100644 --- a/src/config/param_variant.go +++ b/src/config/param_variant.go @@ -38,13 +38,13 @@ func AddVariantParam(cmd *cobra.Command) *StringParam { cobraSettings: cobraSettings{ name: "variant", shorthand: "r", - usage: "indicate the variant to be used by TCR: nice (default) or original", + usage: "indicate the variant to be used by TCR: relaxed (default) or btcr", persistent: true, }, }, v: paramValueString{ value: "", - defaultValue: "nice", + defaultValue: "relaxed", }, } param.addToCommand(cmd) diff --git a/src/config/tcr_config_test.go b/src/config/tcr_config_test.go index 8e75b23c..8e3071ce 100644 --- a/src/config/tcr_config_test.go +++ b/src/config/tcr_config_test.go @@ -244,7 +244,7 @@ func Test_show_tcr_config_with_default_values(t *testing.T) { fmt.Sprintf("%v.tcr.language: %v", prefix, ""), fmt.Sprintf("%v.tcr.toolchain: %v", prefix, ""), fmt.Sprintf("%v.tcr.trace: %v", prefix, "none"), - fmt.Sprintf("%v.tcr.variant: %v", prefix, "nice"), + fmt.Sprintf("%v.tcr.variant: %v", prefix, "relaxed"), fmt.Sprintf("%v.vcs.name: %v", prefix, "git"), } utils.AssertSimpleTrace(t, expected, From 057290f4d3de050c27605051934d405ba3e58b0c Mon Sep 17 00:00:00 2001 From: Ahmad ATWI Date: Wed, 14 Aug 2024 11:00:30 +0200 Subject: [PATCH 10/44] [#674] Transform flavor from string to enum - Implement a new Variant enum - Replace the usage of relaxed and btcr by the enum values --- src/cli/terminal_ui_test.go | 2 +- src/config/param_variant.go | 3 ++- src/config/tcr_config.go | 3 ++- src/config/tcr_config_test.go | 3 ++- src/engine/tcr.go | 9 ++++---- src/engine/tcr_test.go | 14 +++++------ src/engine/tcr_test_fake.go | 3 ++- src/params/params.go | 3 ++- src/params/params_test_data_builder.go | 5 ++-- src/variant/variant.go | 32 ++++++++++++++++++++++++++ 10 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 src/variant/variant.go diff --git a/src/cli/terminal_ui_test.go b/src/cli/terminal_ui_test.go index 2d933a51..d8bee609 100644 --- a/src/cli/terminal_ui_test.go +++ b/src/cli/terminal_ui_test.go @@ -512,7 +512,7 @@ func Test_show_session_info(t *testing.T) { asCyanTrace("Work Directory: fake") + asCyanTrace("Language=fake, Toolchain=fake") + asYellowTrace("VCS \"fake\" is unknown") + - asCyanTrace("Variant is nice") + asCyanTrace("Variant is relaxed") assert.Equal(t, expected, capturer.CaptureStdout(func() { term, _, _ := terminalSetup(*params.AParamSet()) diff --git a/src/config/param_variant.go b/src/config/param_variant.go index 5d320c67..abf5c6da 100644 --- a/src/config/param_variant.go +++ b/src/config/param_variant.go @@ -23,6 +23,7 @@ SOFTWARE. package config import ( + "github.com/murex/tcr/variant" "github.com/spf13/cobra" ) @@ -44,7 +45,7 @@ func AddVariantParam(cmd *cobra.Command) *StringParam { }, v: paramValueString{ value: "", - defaultValue: "relaxed", + defaultValue: string(variant.Relaxed), }, } param.addToCommand(cmd) diff --git a/src/config/tcr_config.go b/src/config/tcr_config.go index 185eb4d2..1ebc45b9 100644 --- a/src/config/tcr_config.go +++ b/src/config/tcr_config.go @@ -28,6 +28,7 @@ import ( "github.com/murex/tcr/settings" "github.com/murex/tcr/toolchain" "github.com/murex/tcr/utils" + "github.com/murex/tcr/variant" "github.com/spf13/cobra" "github.com/spf13/viper" "io" @@ -220,7 +221,7 @@ func UpdateEngineParams(p *params.Params) { p.Toolchain = Config.Toolchain.GetValue() p.PollingPeriod = Config.PollingPeriod.GetValue() p.AutoPush = Config.AutoPush.GetValue() - p.Variant = Config.Variant.GetValue() + p.Variant = variant.Variant(Config.Variant.GetValue()) p.CommitFailures = Config.CommitFailures.GetValue() p.VCS = Config.VCS.GetValue() p.MessageSuffix = Config.MessageSuffix.GetValue() diff --git a/src/config/tcr_config_test.go b/src/config/tcr_config_test.go index 8e3071ce..6d9cedbe 100644 --- a/src/config/tcr_config_test.go +++ b/src/config/tcr_config_test.go @@ -28,6 +28,7 @@ import ( "github.com/murex/tcr/params" "github.com/murex/tcr/toolchain" "github.com/murex/tcr/utils" + "github.com/murex/tcr/variant" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "os" @@ -244,7 +245,7 @@ func Test_show_tcr_config_with_default_values(t *testing.T) { fmt.Sprintf("%v.tcr.language: %v", prefix, ""), fmt.Sprintf("%v.tcr.toolchain: %v", prefix, ""), fmt.Sprintf("%v.tcr.trace: %v", prefix, "none"), - fmt.Sprintf("%v.tcr.variant: %v", prefix, "relaxed"), + fmt.Sprintf("%v.tcr.variant: %v", prefix, variant.Relaxed), fmt.Sprintf("%v.vcs.name: %v", prefix, "git"), } utils.AssertSimpleTrace(t, expected, diff --git a/src/engine/tcr.go b/src/engine/tcr.go index aecba66b..c442ebd4 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -40,6 +40,7 @@ import ( "github.com/murex/tcr/toolchain" "github.com/murex/tcr/toolchain/command" "github.com/murex/tcr/ui" + "github.com/murex/tcr/variant" "github.com/murex/tcr/vcs" "github.com/murex/tcr/vcs/factory" "gopkg.in/tomb.v2" @@ -58,7 +59,7 @@ type ( ToggleAutoPush() SetAutoPush(flag bool) SetCommitOnFail(flag bool) - SetVariant(variant string) + SetVariant(variant variant.Variant) GetCurrentRole() role.Role RunAsDriver() RunAsNavigator() @@ -93,7 +94,7 @@ type ( // before starting a new one roleMutex sync.Mutex commitOnFail bool - variant string + variant variant.Variant messageSuffix string // shoot channel is used for handling interruptions coming from the UI shoot chan bool @@ -108,7 +109,7 @@ type ( } ) -func (tcr *TCREngine) SetVariant(variant string) { +func (tcr *TCREngine) SetVariant(variant variant.Variant) { tcr.variant = variant } @@ -685,7 +686,7 @@ func (tcr *TCREngine) GetSessionInfo() SessionInfo { VCSSessionSummary: tcr.vcs.SessionSummary(), GitAutoPush: tcr.vcs.IsAutoPushEnabled(), CommitOnFail: tcr.commitOnFail, - Variant: tcr.variant, + Variant: string(tcr.variant), MessageSuffix: tcr.messageSuffix, } } diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 795c101d..8462a366 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -35,6 +35,7 @@ import ( "github.com/murex/tcr/toolchain" "github.com/murex/tcr/toolchain/command" "github.com/murex/tcr/ui" + "github.com/murex/tcr/variant" "github.com/murex/tcr/vcs" "github.com/murex/tcr/vcs/factory" "github.com/murex/tcr/vcs/fake" @@ -469,17 +470,16 @@ func Test_set_commit_on_fail(t *testing.T) { func Test_set_variant(t *testing.T) { var tcr TCRInterface testFlags := []struct { - desc string - variant string + variant variant.Variant }{ - {"Nice Variant", "nice"}, - {"Original Variant", "original"}, + {variant.Relaxed}, + {variant.BTCR}, } for _, tt := range testFlags { - t.Run(tt.desc, func(t *testing.T) { + t.Run(fmt.Sprintf("Variant %v", tt.variant), func(t *testing.T) { tcr, _ = initTCREngineWithFakes(nil, nil, nil, nil) tcr.SetVariant(tt.variant) - assert.Equal(t, tt.variant, tcr.GetSessionInfo().Variant) + assert.Equal(t, string(tt.variant), tcr.GetSessionInfo().Variant) }) } } @@ -536,7 +536,7 @@ func Test_get_session_info(t *testing.T) { ToolchainName: "fake-toolchain", VCSName: fake.Name, VCSSessionSummary: "VCS session \"" + fake.Name + "\"", - Variant: "nice", + Variant: string(variant.Relaxed), GitAutoPush: false, } assert.Equal(t, expected, tcr.GetSessionInfo()) diff --git a/src/engine/tcr_test_fake.go b/src/engine/tcr_test_fake.go index d54cf275..2768819a 100644 --- a/src/engine/tcr_test_fake.go +++ b/src/engine/tcr_test_fake.go @@ -30,6 +30,7 @@ import ( "github.com/murex/tcr/status" "github.com/murex/tcr/timer" "github.com/murex/tcr/ui" + "github.com/murex/tcr/variant" ) // TCRCall is used to track calls to TCR operations @@ -79,7 +80,7 @@ func NewFakeTCREngine() *FakeTCREngine { VCSSessionSummary: "VCS session \"fake\"", GitAutoPush: false, CommitOnFail: false, - Variant: "nice", + Variant: string(variant.Relaxed), }, } } diff --git a/src/params/params.go b/src/params/params.go index 55b9f85d..16533e95 100644 --- a/src/params/params.go +++ b/src/params/params.go @@ -24,6 +24,7 @@ package params import ( "github.com/murex/tcr/runmode" + "github.com/murex/tcr/variant" "time" ) @@ -36,7 +37,7 @@ type Params struct { Toolchain string MobTurnDuration time.Duration AutoPush bool - Variant string + Variant variant.Variant CommitFailures bool PollingPeriod time.Duration Mode runmode.RunMode diff --git a/src/params/params_test_data_builder.go b/src/params/params_test_data_builder.go index 47c12709..862c20b9 100644 --- a/src/params/params_test_data_builder.go +++ b/src/params/params_test_data_builder.go @@ -26,6 +26,7 @@ package params import ( "github.com/murex/tcr/runmode" + "github.com/murex/tcr/variant" "time" ) @@ -39,7 +40,7 @@ func AParamSet(builders ...func(params *Params)) *Params { Toolchain: "", MobTurnDuration: 0, AutoPush: false, - Variant: "nice", + Variant: variant.Relaxed, PollingPeriod: 0, Mode: runmode.OneShot{}, VCS: "git", @@ -109,7 +110,7 @@ func WithAutoPush(value bool) func(params *Params) { } // WithVariant sets the provided value as the variant to be used -func WithVariant(variant string) func(params *Params) { +func WithVariant(variant variant.Variant) func(params *Params) { return func(params *Params) { params.Variant = variant } diff --git a/src/variant/variant.go b/src/variant/variant.go new file mode 100644 index 00000000..04e952ec --- /dev/null +++ b/src/variant/variant.go @@ -0,0 +1,32 @@ +/* +Copyright (c) 2024 Murex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package variant + +// Variant represents the possible values for the TCR Variant +// https://medium.com/@tdeniffel/tcr-variants-test-commit-revert-bf6bd84b17d3 +type Variant string + +const ( + Relaxed Variant = "relaxed" + BTCR Variant = "btcr" +) From 5f7f35fcb09ee36af1175212e392f8b1543b6894 Mon Sep 17 00:00:00 2001 From: Ahmad ATWI Date: Wed, 14 Aug 2024 11:14:44 +0200 Subject: [PATCH 11/44] [#674] Transform flavor from string to enum - Implement the function Name that returns the value of the Variant as a string - Use the function in the code --- src/config/param_variant.go | 2 +- src/engine/tcr_test.go | 2 +- src/engine/tcr_test_fake.go | 2 +- src/variant/variant.go | 4 ++++ src/variant/variant_test.go | 45 +++++++++++++++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 src/variant/variant_test.go diff --git a/src/config/param_variant.go b/src/config/param_variant.go index abf5c6da..5e45acb3 100644 --- a/src/config/param_variant.go +++ b/src/config/param_variant.go @@ -45,7 +45,7 @@ func AddVariantParam(cmd *cobra.Command) *StringParam { }, v: paramValueString{ value: "", - defaultValue: string(variant.Relaxed), + defaultValue: variant.Relaxed.Name(), }, } param.addToCommand(cmd) diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 8462a366..7d3de7cb 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -536,7 +536,7 @@ func Test_get_session_info(t *testing.T) { ToolchainName: "fake-toolchain", VCSName: fake.Name, VCSSessionSummary: "VCS session \"" + fake.Name + "\"", - Variant: string(variant.Relaxed), + Variant: variant.Relaxed.Name(), GitAutoPush: false, } assert.Equal(t, expected, tcr.GetSessionInfo()) diff --git a/src/engine/tcr_test_fake.go b/src/engine/tcr_test_fake.go index 2768819a..174cdc48 100644 --- a/src/engine/tcr_test_fake.go +++ b/src/engine/tcr_test_fake.go @@ -80,7 +80,7 @@ func NewFakeTCREngine() *FakeTCREngine { VCSSessionSummary: "VCS session \"fake\"", GitAutoPush: false, CommitOnFail: false, - Variant: string(variant.Relaxed), + Variant: variant.Relaxed.Name(), }, } } diff --git a/src/variant/variant.go b/src/variant/variant.go index 04e952ec..cf85130c 100644 --- a/src/variant/variant.go +++ b/src/variant/variant.go @@ -26,6 +26,10 @@ package variant // https://medium.com/@tdeniffel/tcr-variants-test-commit-revert-bf6bd84b17d3 type Variant string +func (v Variant) Name() string { + return string(v) +} + const ( Relaxed Variant = "relaxed" BTCR Variant = "btcr" diff --git a/src/variant/variant_test.go b/src/variant/variant_test.go new file mode 100644 index 00000000..ce24b9b0 --- /dev/null +++ b/src/variant/variant_test.go @@ -0,0 +1,45 @@ +/* +Copyright (c) 2023 Murex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package variant + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_get_variant_name(t *testing.T) { + tests := []struct { + desc string + variant Variant + expected string + }{ + {"relaxed", Relaxed, "relaxed"}, + {"btcr", BTCR, "btcr"}, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + assert.Equal(t, test.expected, test.variant.Name()) + }) + } +} From c8cd131045b2386112caaf43b266d24e7b31e7be Mon Sep 17 00:00:00 2001 From: Ahmad ATWI Date: Wed, 11 Sep 2024 10:37:29 +0200 Subject: [PATCH 12/44] [#674] Add tests to assert source files are being reverted - Check whether a file contains a 'test' to know if it is a test file - Added tests for different revert configuration of the relaxed TCR --- src/engine/tcr_test.go | 62 +++++++++++++++++++++++++++++- src/language/language_test_fake.go | 9 +++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 7d3de7cb..3689e318 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -281,6 +281,48 @@ func Test_tcr_revert_end_state_with_commit_on_fail_enabled(t *testing.T) { } } +func Test_relaxed_source_revert(t *testing.T) { + tcr, vcsFake := initTCREngineWithFakes(nil, nil, nil, nil) + tcr.revert(*events.ATcrEvent()) + assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) +} + +func Test_relaxed_source_reverts_only_source_files(t *testing.T) { + sniffer := report.NewSniffer( + func(msg report.Message) bool { + return msg.Type.Category == report.Warning && + msg.Payload.ToString() == "1 file(s) reverted" + }, + ) + tcr, vcsFake := initTCREngineWithFakesWithFileDiffs(nil, nil, nil, nil, + vcs.FileDiffs{ + vcs.NewFileDiff("fake-src", 1, 1), + vcs.NewFileDiff("fake-test", 1, 1), + }) + tcr.revert(*events.ATcrEvent()) + sniffer.Stop() + assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) + assert.Equal(t, 1, sniffer.GetMatchCount()) +} + +func Test_relaxed_source_reverts_many_source_files(t *testing.T) { + sniffer := report.NewSniffer( + func(msg report.Message) bool { + return msg.Type.Category == report.Warning && + msg.Payload.ToString() == "2 file(s) reverted" + }, + ) + tcr, vcsFake := initTCREngineWithFakesWithFileDiffs(nil, nil, nil, nil, + vcs.FileDiffs{ + vcs.NewFileDiff("fake-src", 1, 1), + vcs.NewFileDiff("fake2-src", 1, 1), + }) + tcr.revert(*events.ATcrEvent()) + sniffer.Stop() + assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) + assert.Equal(t, 1, sniffer.GetMatchCount()) +} + func Test_tcr_cycle_end_state(t *testing.T) { testFlags := []struct { desc string @@ -342,11 +384,12 @@ func Test_tcr_cycle_end_state(t *testing.T) { } } -func initTCREngineWithFakes( +func initTCREngineWithFakesWithFileDiffs( p *params.Params, toolchainFailures toolchain.Operations, vcsFailures fake.Commands, logItems vcs.LogItems, + fileDiffs vcs.FileDiffs, ) (*TCREngine, *fake.VCSFake) { tchn := registerFakeToolchain(toolchainFailures) lang := registerFakeLanguage(tchn) @@ -381,7 +424,7 @@ func initTCREngineWithFakes( factory.InitVCS = func(_ string, _ string) (vcs.Interface, error) { fakeSettings := fake.Settings{ FailingCommands: vcsFailures, - ChangedFiles: vcs.FileDiffs{vcs.NewFileDiff("fake-src", 1, 1)}, + ChangedFiles: fileDiffs, Logs: logItems, } vcsFake = fake.NewVCSFake(fakeSettings) @@ -393,6 +436,21 @@ func initTCREngineWithFakes( tcr.fsWatchRearmDelay = 0 tcr.traceReporterWaitingTime = 0 return tcr, vcsFake + +} + +func initTCREngineWithFakes( + p *params.Params, + toolchainFailures toolchain.Operations, + vcsFailures fake.Commands, + logItems vcs.LogItems, +) (*TCREngine, *fake.VCSFake) { + return initTCREngineWithFakesWithFileDiffs( + p, + toolchainFailures, + vcsFailures, + logItems, + vcs.FileDiffs{vcs.NewFileDiff("fake-src", 1, 1)}) } func registerFakeToolchain(failures toolchain.Operations) string { diff --git a/src/language/language_test_fake.go b/src/language/language_test_fake.go index 20dc2252..2d4d6042 100644 --- a/src/language/language_test_fake.go +++ b/src/language/language_test_fake.go @@ -24,7 +24,10 @@ SOFTWARE. package language -import "github.com/murex/tcr/toolchain" +import ( + "github.com/murex/tcr/toolchain" + "strings" +) type allFilesFunc func() ([]string, error) @@ -84,8 +87,8 @@ func (fl *FakeLanguage) AllTestFiles() (result []string, err error) { } // IsSrcFile returns true if the provided filePath is recognized as a source file for this language -func (fl *FakeLanguage) IsSrcFile(_ string) bool { - return true +func (fl *FakeLanguage) IsSrcFile(name string) bool { + return !strings.Contains(name, "test") } // GetName uses real Language behaviour From 42a65219f1c077f5a65645be9b21c618ad092499 Mon Sep 17 00:00:00 2001 From: Ahmad ATWI Date: Wed, 11 Sep 2024 10:57:59 +0200 Subject: [PATCH 13/44] [#674] Refactor tests - Replace tests by parameterized tests - Added new test that covers not reverting test files --- src/engine/tcr_test.go | 63 +++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 3689e318..9613cdde 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -287,40 +287,53 @@ func Test_relaxed_source_revert(t *testing.T) { assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) } -func Test_relaxed_source_reverts_only_source_files(t *testing.T) { - sniffer := report.NewSniffer( - func(msg report.Message) bool { - return msg.Type.Category == report.Warning && - msg.Payload.ToString() == "1 file(s) reverted" - }, - ) +func Test_relaxed_doesnt_revert_test_files(t *testing.T) { tcr, vcsFake := initTCREngineWithFakesWithFileDiffs(nil, nil, nil, nil, vcs.FileDiffs{ - vcs.NewFileDiff("fake-src", 1, 1), vcs.NewFileDiff("fake-test", 1, 1), }) tcr.revert(*events.ATcrEvent()) - sniffer.Stop() - assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) - assert.Equal(t, 1, sniffer.GetMatchCount()) + assert.NotEqual(t, fake.RestoreCommand, vcsFake.GetLastCommand()) } -func Test_relaxed_source_reverts_many_source_files(t *testing.T) { - sniffer := report.NewSniffer( - func(msg report.Message) bool { - return msg.Type.Category == report.Warning && - msg.Payload.ToString() == "2 file(s) reverted" +func Test_relaxed_reverts(t *testing.T) { + testFlags := []struct { + description string + fileDiffs vcs.FileDiffs + expectedRevertCount int + }{ + { + description: "One source file and one test file", + fileDiffs: vcs.FileDiffs{ + vcs.NewFileDiff("fake-src", 1, 1), + vcs.NewFileDiff("fake-test", 1, 1), + }, + expectedRevertCount: 1, + }, { + description: "Two source files and no test files", + fileDiffs: vcs.FileDiffs{ + vcs.NewFileDiff("fake-src", 1, 1), + vcs.NewFileDiff("fake2-src", 1, 1), + }, + expectedRevertCount: 2, }, - ) - tcr, vcsFake := initTCREngineWithFakesWithFileDiffs(nil, nil, nil, nil, - vcs.FileDiffs{ - vcs.NewFileDiff("fake-src", 1, 1), - vcs.NewFileDiff("fake2-src", 1, 1), + } + + for _, tt := range testFlags { + t.Run(tt.description, func(t *testing.T) { + sniffer := report.NewSniffer( + func(msg report.Message) bool { + return msg.Type.Category == report.Warning && + msg.Payload.ToString() == fmt.Sprintf("%d file(s) reverted", tt.expectedRevertCount) + }, + ) + tcr, vcsFake := initTCREngineWithFakesWithFileDiffs(nil, nil, nil, nil, tt.fileDiffs) + tcr.revert(*events.ATcrEvent()) + sniffer.Stop() + assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) + assert.Equal(t, 1, sniffer.GetMatchCount()) }) - tcr.revert(*events.ATcrEvent()) - sniffer.Stop() - assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) - assert.Equal(t, 1, sniffer.GetMatchCount()) + } } func Test_tcr_cycle_end_state(t *testing.T) { From c1aac0a462a5754dbe68242807575b4b1f5382b7 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Wed, 11 Sep 2024 11:20:35 +0200 Subject: [PATCH 14/44] [#674] Implement revert for BTCR variant - Add tests - Inline revertSrc() file into revert() - Extract shouldRevertFile() - Extract noFilesRevertedMessage() --- src/engine/tcr.go | 56 ++++++++++++++++++++++++------------------ src/engine/tcr_test.go | 30 +++++++++++++++++++--- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/engine/tcr.go b/src/engine/tcr.go index c442ebd4..fce84f66 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -609,7 +609,38 @@ func (tcr *TCREngine) revert(event events.TCREvent) { } tcr.handleError(tcr.vcsPushAuto(), false, status.VCSError) } - tcr.revertSrcFiles() + + diffs, err := tcr.vcs.Diff() + tcr.handleError(err, false, status.VCSError) + if err != nil { + return + } + var reverted int + for _, diff := range diffs { + if tcr.shouldRevertFile(diff.Path) { + err := tcr.revertFile(diff.Path) + tcr.handleError(err, false, status.VCSError) + if err == nil { + reverted++ + } + } + } + if reverted > 0 { + report.PostWarning(reverted, " file(s) reverted") + } else { + report.PostInfo(tcr.noFilesRevertedMessage()) + } +} + +func (tcr *TCREngine) noFilesRevertedMessage() string { + if tcr.variant == variant.Relaxed { + return "No file reverted (only test files were updated since last commit)" + } + return "No file reverted" +} + +func (tcr *TCREngine) shouldRevertFile(path string) bool { + return tcr.variant == variant.BTCR || tcr.language.IsSrcFile(path) } func (tcr *TCREngine) commitTestBreakingChanges(event events.TCREvent) (err error) { @@ -647,29 +678,6 @@ func (tcr *TCREngine) commitTestBreakingChanges(event events.TCREvent) (err erro return err } -func (tcr *TCREngine) revertSrcFiles() { - diffs, err := tcr.vcs.Diff() - tcr.handleError(err, false, status.VCSError) - if err != nil { - return - } - var reverted int - for _, diff := range diffs { - if tcr.language.IsSrcFile(diff.Path) { - err := tcr.revertFile(diff.Path) - tcr.handleError(err, false, status.VCSError) - if err == nil { - reverted++ - } - } - } - if reverted > 0 { - report.PostWarning(reverted, " file(s) reverted") - } else { - report.PostInfo("No file reverted (only test files were updated since last commit)") - } -} - func (tcr *TCREngine) revertFile(file string) error { return tcr.vcs.Restore(file) } diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 9613cdde..e994cfca 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -299,18 +299,40 @@ func Test_relaxed_doesnt_revert_test_files(t *testing.T) { func Test_relaxed_reverts(t *testing.T) { testFlags := []struct { description string + variant variant.Variant fileDiffs vcs.FileDiffs expectedRevertCount int }{ { description: "One source file and one test file", + variant: variant.Relaxed, fileDiffs: vcs.FileDiffs{ vcs.NewFileDiff("fake-src", 1, 1), vcs.NewFileDiff("fake-test", 1, 1), }, expectedRevertCount: 1, - }, { + }, + { + description: "Two source files and no test files", + variant: variant.Relaxed, + fileDiffs: vcs.FileDiffs{ + vcs.NewFileDiff("fake-src", 1, 1), + vcs.NewFileDiff("fake2-src", 1, 1), + }, + expectedRevertCount: 2, + }, + { + description: "One source file and one test file", + variant: variant.BTCR, + fileDiffs: vcs.FileDiffs{ + vcs.NewFileDiff("fake-src", 1, 1), + vcs.NewFileDiff("fake-test", 1, 1), + }, + expectedRevertCount: 2, + }, + { description: "Two source files and no test files", + variant: variant.BTCR, fileDiffs: vcs.FileDiffs{ vcs.NewFileDiff("fake-src", 1, 1), vcs.NewFileDiff("fake2-src", 1, 1), @@ -320,14 +342,16 @@ func Test_relaxed_reverts(t *testing.T) { } for _, tt := range testFlags { - t.Run(tt.description, func(t *testing.T) { + t.Run(tt.description+" in variant "+string(tt.variant), func(t *testing.T) { sniffer := report.NewSniffer( func(msg report.Message) bool { return msg.Type.Category == report.Warning && msg.Payload.ToString() == fmt.Sprintf("%d file(s) reverted", tt.expectedRevertCount) }, ) - tcr, vcsFake := initTCREngineWithFakesWithFileDiffs(nil, nil, nil, nil, tt.fileDiffs) + tcr, vcsFake := initTCREngineWithFakesWithFileDiffs( + params.AParamSet(params.WithVariant(tt.variant)), + nil, nil, nil, tt.fileDiffs) tcr.revert(*events.ATcrEvent()) sniffer.Stop() assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) From 750750a746a928031ee71c7c45e777e3307a0511 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Thu, 5 Sep 2024 09:16:48 +0200 Subject: [PATCH 15/44] [#674] Create mermaid diagrams for 3 main TCR variants (original, btcr and relaxed) --- variants-doc/variant-btcr.mermaid | 25 +++++++++++++++++++++++++ variants-doc/variant-original.mermaid | 20 ++++++++++++++++++++ variants-doc/variant-relaxed.mermaid | 25 +++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 variants-doc/variant-btcr.mermaid create mode 100644 variants-doc/variant-original.mermaid create mode 100644 variants-doc/variant-relaxed.mermaid diff --git a/variants-doc/variant-btcr.mermaid b/variants-doc/variant-btcr.mermaid new file mode 100644 index 00000000..6fa51726 --- /dev/null +++ b/variants-doc/variant-btcr.mermaid @@ -0,0 +1,25 @@ +graph LR + START((start)) + BUILD{build} + TEST{test} + COMMIT["✅ commit (src + tests)"] + REVERT["❌ revert (src + tests)"] + END((end)) + START --> BUILD + BUILD -->|pass| TEST + BUILD -->|fail| END + TEST -->|pass| COMMIT + TEST -->|fail| REVERT + COMMIT --> END + REVERT --> END + classDef boundaryclass fill: #555555 + classDef buildclass fill: #0077CC + classDef testclass fill: #0077CC + classDef okclass fill: #006600 + classDef failclass fill: #660000 + class START boundaryclass + class BUILD buildclass + class TEST testclass + class COMMIT okclass + class REVERT failclass + class END boundaryclass diff --git a/variants-doc/variant-original.mermaid b/variants-doc/variant-original.mermaid new file mode 100644 index 00000000..c5c1682a --- /dev/null +++ b/variants-doc/variant-original.mermaid @@ -0,0 +1,20 @@ +graph LR + START((start)) + TEST{test} + COMMIT["✅ commit src + tests)"] + REVERT["❌ revert (src + tests)"] + END((end)) + START --> TEST + TEST -->|"pass"| COMMIT + TEST -->|"fail"| REVERT + COMMIT --> END + REVERT --> END + classDef boundaryclass fill: #555555 + classDef testclass fill: #0077CC + classDef okclass fill: #006600 + classDef failclass fill: #660000 + class START boundaryclass + class TEST testclass + class COMMIT okclass + class REVERT failclass + class END boundaryclass diff --git a/variants-doc/variant-relaxed.mermaid b/variants-doc/variant-relaxed.mermaid new file mode 100644 index 00000000..ba0f59e2 --- /dev/null +++ b/variants-doc/variant-relaxed.mermaid @@ -0,0 +1,25 @@ +graph LR + START((start)) + BUILD{build} + TEST{test} + COMMIT["✅ commit (src + tests)"] + REVERT["❌ revert (src)"] + END((end)) + START --> BUILD + BUILD -->|pass| TEST + BUILD -->|fail| END + TEST -->|pass| COMMIT + TEST -->|fail| REVERT + COMMIT --> END + REVERT --> END + classDef boundaryclass fill: #555555 + classDef buildclass fill: #0077CC + classDef testclass fill: #0077CC + classDef okclass fill: #006600 + classDef failclass fill: #660000 + class START boundaryclass + class BUILD buildclass + class TEST testclass + class COMMIT okclass + class REVERT failclass + class END boundaryclass From 6d3c39d5ef33237a94c01fea4c34e3efffd7da7e Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Fri, 6 Sep 2024 15:39:39 +0200 Subject: [PATCH 16/44] [#674] Change mermaid diagrams to state diagrams instead of graphs --- variants-doc/variant-btcr.mermaid | 45 ++++++++++++--------------- variants-doc/variant-original.mermaid | 36 ++++++++++----------- variants-doc/variant-relaxed.mermaid | 45 ++++++++++++--------------- 3 files changed, 56 insertions(+), 70 deletions(-) diff --git a/variants-doc/variant-btcr.mermaid b/variants-doc/variant-btcr.mermaid index 6fa51726..8b42b22a 100644 --- a/variants-doc/variant-btcr.mermaid +++ b/variants-doc/variant-btcr.mermaid @@ -1,25 +1,20 @@ -graph LR - START((start)) - BUILD{build} - TEST{test} - COMMIT["✅ commit (src + tests)"] - REVERT["❌ revert (src + tests)"] - END((end)) - START --> BUILD - BUILD -->|pass| TEST - BUILD -->|fail| END - TEST -->|pass| COMMIT - TEST -->|fail| REVERT - COMMIT --> END - REVERT --> END - classDef boundaryclass fill: #555555 - classDef buildclass fill: #0077CC - classDef testclass fill: #0077CC - classDef okclass fill: #006600 - classDef failclass fill: #660000 - class START boundaryclass - class BUILD buildclass - class TEST testclass - class COMMIT okclass - class REVERT failclass - class END boundaryclass +stateDiagram-v2 + direction LR + state "⚙️ build" as Build + state "⚙️ test" as Test + state "✅ commit (src + tests)" as Commit + state "❌ revert (src + tests)" as Revert + [*] --> Build + Build --> Test: pass + Build --> [*]: fail + Test --> Commit: pass + Test --> Revert: fail + Commit --> [*] + Revert --> [*] + classDef actionClass fill: #0077CC + classDef okClass fill: #006600 + classDef failClass fill: #660000 + class Build actionClass + class Test actionClass + class Commit okClass + class Revert failClass diff --git a/variants-doc/variant-original.mermaid b/variants-doc/variant-original.mermaid index c5c1682a..e36679ce 100644 --- a/variants-doc/variant-original.mermaid +++ b/variants-doc/variant-original.mermaid @@ -1,20 +1,16 @@ -graph LR - START((start)) - TEST{test} - COMMIT["✅ commit src + tests)"] - REVERT["❌ revert (src + tests)"] - END((end)) - START --> TEST - TEST -->|"pass"| COMMIT - TEST -->|"fail"| REVERT - COMMIT --> END - REVERT --> END - classDef boundaryclass fill: #555555 - classDef testclass fill: #0077CC - classDef okclass fill: #006600 - classDef failclass fill: #660000 - class START boundaryclass - class TEST testclass - class COMMIT okclass - class REVERT failclass - class END boundaryclass +stateDiagram-v2 + direction LR + state "⚙️ test" as Test + state "✅ commit (src + tests)" as Commit + state "❌ revert (src + tests)" as Revert + [*] --> Test + Test --> Commit: pass + Test --> Revert: fail + Commit --> [*] + Revert --> [*] + classDef actionClass fill: #0077CC + classDef okClass fill: #006600 + classDef failClass fill: #660000 + class Test actionClass + class Commit okClass + class Revert failClass diff --git a/variants-doc/variant-relaxed.mermaid b/variants-doc/variant-relaxed.mermaid index ba0f59e2..7544e60b 100644 --- a/variants-doc/variant-relaxed.mermaid +++ b/variants-doc/variant-relaxed.mermaid @@ -1,25 +1,20 @@ -graph LR - START((start)) - BUILD{build} - TEST{test} - COMMIT["✅ commit (src + tests)"] - REVERT["❌ revert (src)"] - END((end)) - START --> BUILD - BUILD -->|pass| TEST - BUILD -->|fail| END - TEST -->|pass| COMMIT - TEST -->|fail| REVERT - COMMIT --> END - REVERT --> END - classDef boundaryclass fill: #555555 - classDef buildclass fill: #0077CC - classDef testclass fill: #0077CC - classDef okclass fill: #006600 - classDef failclass fill: #660000 - class START boundaryclass - class BUILD buildclass - class TEST testclass - class COMMIT okclass - class REVERT failclass - class END boundaryclass +stateDiagram-v2 + direction LR + state "⚙️ build" as Build + state "⚙️ test" as Test + state "✅ commit (src + tests)" as Commit + state "❌ revert (src)" as Revert + [*] --> Build + Build --> Test: pass + Build --> [*]: fail + Test --> Commit: pass + Test --> Revert: fail + Commit --> [*] + Revert --> [*] + classDef actionClass fill: #0077CC + classDef okClass fill: #006600 + classDef failClass fill: #660000 + class Build actionClass + class Test actionClass + class Commit okClass + class Revert failClass From cb964363512a38b23b8be6659b048326267b2190 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Fri, 6 Sep 2024 18:39:41 +0200 Subject: [PATCH 17/44] [#674] Change mermaid diagram extensions to .mmd instead of .mermaid --- variants-doc/{variant-btcr.mermaid => variant-btcr.mmd} | 0 variants-doc/{variant-original.mermaid => variant-original.mmd} | 0 variants-doc/{variant-relaxed.mermaid => variant-relaxed.mmd} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename variants-doc/{variant-btcr.mermaid => variant-btcr.mmd} (100%) rename variants-doc/{variant-original.mermaid => variant-original.mmd} (100%) rename variants-doc/{variant-relaxed.mermaid => variant-relaxed.mmd} (100%) diff --git a/variants-doc/variant-btcr.mermaid b/variants-doc/variant-btcr.mmd similarity index 100% rename from variants-doc/variant-btcr.mermaid rename to variants-doc/variant-btcr.mmd diff --git a/variants-doc/variant-original.mermaid b/variants-doc/variant-original.mmd similarity index 100% rename from variants-doc/variant-original.mermaid rename to variants-doc/variant-original.mmd diff --git a/variants-doc/variant-relaxed.mermaid b/variants-doc/variant-relaxed.mmd similarity index 100% rename from variants-doc/variant-relaxed.mermaid rename to variants-doc/variant-relaxed.mmd From eb7bf5a741529fd36af1afa696e90a363250425d Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Fri, 6 Sep 2024 18:40:47 +0200 Subject: [PATCH 18/44] [#674] Add script for generating png files from mmd --- variants-doc/generate_png.bash | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 variants-doc/generate_png.bash diff --git a/variants-doc/generate_png.bash b/variants-doc/generate_png.bash new file mode 100644 index 00000000..61ec0621 --- /dev/null +++ b/variants-doc/generate_png.bash @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +echo "Installing mermaid-cli" +npm update -g @mermaid-js/mermaid-cli || echo "Failed to install mermaid-cli. Aborting" + +for mmd_file in ./*.mmd; do + png_file="${mmd_file%.mmd}.png" + echo "- Generating ${png_file} from ${mmd_file}" + mmdc --input "${mmd_file}" --output "${png_file}" --theme dark --backgroundColor transparent +done From 0aa47702820b680666e6bec4401cf8ff3bc08393 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Fri, 6 Sep 2024 18:41:05 +0200 Subject: [PATCH 19/44] [#674] Add generated png files --- variants-doc/variant-btcr.png | Bin 0 -> 19941 bytes variants-doc/variant-original.png | Bin 0 -> 12855 bytes variants-doc/variant-relaxed.png | Bin 0 -> 19969 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 variants-doc/variant-btcr.png create mode 100644 variants-doc/variant-original.png create mode 100644 variants-doc/variant-relaxed.png diff --git a/variants-doc/variant-btcr.png b/variants-doc/variant-btcr.png new file mode 100644 index 0000000000000000000000000000000000000000..c96080ccbc68b4b3390d904bd82ea4be3531b07f GIT binary patch literal 19941 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 literal 0 HcmV?d00001 diff --git a/variants-doc/variant-relaxed.png b/variants-doc/variant-relaxed.png new file mode 100644 index 0000000000000000000000000000000000000000..26a47f90ed82a21bd5e03e926aadddfc526fee41 GIT binary patch literal 19969 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 Vs!VH)12f$q_BI!hPptg^{1@X Date: Fri, 6 Sep 2024 18:41:39 +0200 Subject: [PATCH 20/44] [#674] Add summary page describing available TCR variants --- variants-doc/tcr_variants.md | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 variants-doc/tcr_variants.md diff --git a/variants-doc/tcr_variants.md b/variants-doc/tcr_variants.md new file mode 100644 index 00000000..345f1908 --- /dev/null +++ b/variants-doc/tcr_variants.md @@ -0,0 +1,45 @@ +# TCR Variants + +TCR tool can run several variants of TCR, as described in +[this blog post](https://medium.com/@tdeniffel/tcr-variants-test-commit-revert-bf6bd84b17d3) +by Thomas Deniffel. + +We encourage you to read through Thomas's blog post for a deeper understanding of how these +variants differ and what each of them can bring. + +The selection of which variant TCR will be applying is done at launch time through +TCR's tool `--variant` (or `-r`) command-line option. + +The variants currently supported by TCR tool are the following: + +- The Original +- BTCR +- The Relaxed (default) + +The state diagrams below summarize the behavior of each variant. + +## The Original + +```shell +tcr --variant=original +``` + +![TCR Original variant](./variant-original.png) + +## BTCR - Build && Test && Commit || Revert + +```shell +tcr --variant=btcr +``` + +![TCR BTCR variant](./variant-btcr.png) + +## The Relaxed + +```shell +tcr --variant=relaxed +``` + +The above option `--variant=relaxed` may be omitted as this is the default variant. + +![TCR Relaxed variant](./variant-relaxed.png) From 21c063686eafca8a61745af768144c027d52be5f Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Tue, 10 Sep 2024 09:25:45 +0200 Subject: [PATCH 21/44] [#674] Make mermaid-generated images available to webapp --- variants-doc/generate_png.bash | 10 ---------- variants-doc/mmd/generate_png.bash | 14 ++++++++++++++ variants-doc/{ => mmd}/variant-btcr.mmd | 0 variants-doc/{ => mmd}/variant-original.mmd | 0 variants-doc/{ => mmd}/variant-relaxed.mmd | 0 variants-doc/tcr_variants.md | 6 +++--- .../src/assets/images}/variant-btcr.png | Bin .../src/assets/images}/variant-original.png | Bin .../src/assets/images}/variant-relaxed.png | Bin 9 files changed, 17 insertions(+), 13 deletions(-) delete mode 100644 variants-doc/generate_png.bash create mode 100644 variants-doc/mmd/generate_png.bash rename variants-doc/{ => mmd}/variant-btcr.mmd (100%) rename variants-doc/{ => mmd}/variant-original.mmd (100%) rename variants-doc/{ => mmd}/variant-relaxed.mmd (100%) rename {variants-doc => webapp/src/assets/images}/variant-btcr.png (100%) rename {variants-doc => webapp/src/assets/images}/variant-original.png (100%) rename {variants-doc => webapp/src/assets/images}/variant-relaxed.png (100%) diff --git a/variants-doc/generate_png.bash b/variants-doc/generate_png.bash deleted file mode 100644 index 61ec0621..00000000 --- a/variants-doc/generate_png.bash +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -echo "Installing mermaid-cli" -npm update -g @mermaid-js/mermaid-cli || echo "Failed to install mermaid-cli. Aborting" - -for mmd_file in ./*.mmd; do - png_file="${mmd_file%.mmd}.png" - echo "- Generating ${png_file} from ${mmd_file}" - mmdc --input "${mmd_file}" --output "${png_file}" --theme dark --backgroundColor transparent -done diff --git a/variants-doc/mmd/generate_png.bash b/variants-doc/mmd/generate_png.bash new file mode 100644 index 00000000..3b9564e4 --- /dev/null +++ b/variants-doc/mmd/generate_png.bash @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +echo "Installing mermaid-cli" +npm update -g @mermaid-js/mermaid-cli || echo "Failed to install mermaid-cli. Aborting" + +repo_root_dir="$(git rev-parse --show-toplevel)" +webapp_images_dir="${repo_root_dir}/webapp/src/assets/images" + +echo "Generating files into ${webapp_images_dir}" +for mmd_file in ./*.mmd; do + png_file="${mmd_file%.mmd}.png" + echo "- Generating ${png_file} from ${mmd_file}" + mmdc --input "${mmd_file}" --output "${webapp_images_dir}/${png_file}" --theme dark --backgroundColor transparent +done diff --git a/variants-doc/variant-btcr.mmd b/variants-doc/mmd/variant-btcr.mmd similarity index 100% rename from variants-doc/variant-btcr.mmd rename to variants-doc/mmd/variant-btcr.mmd diff --git a/variants-doc/variant-original.mmd b/variants-doc/mmd/variant-original.mmd similarity index 100% rename from variants-doc/variant-original.mmd rename to variants-doc/mmd/variant-original.mmd diff --git a/variants-doc/variant-relaxed.mmd b/variants-doc/mmd/variant-relaxed.mmd similarity index 100% rename from variants-doc/variant-relaxed.mmd rename to variants-doc/mmd/variant-relaxed.mmd diff --git a/variants-doc/tcr_variants.md b/variants-doc/tcr_variants.md index 345f1908..7f6f6aec 100644 --- a/variants-doc/tcr_variants.md +++ b/variants-doc/tcr_variants.md @@ -24,7 +24,7 @@ The state diagrams below summarize the behavior of each variant. tcr --variant=original ``` -![TCR Original variant](./variant-original.png) +![TCR Original variant](../webapp/src/assets/images/variant-original.png) ## BTCR - Build && Test && Commit || Revert @@ -32,7 +32,7 @@ tcr --variant=original tcr --variant=btcr ``` -![TCR BTCR variant](./variant-btcr.png) +![TCR BTCR variant](../webapp/src/assets/images/variant-btcr.png) ## The Relaxed @@ -42,4 +42,4 @@ tcr --variant=relaxed The above option `--variant=relaxed` may be omitted as this is the default variant. -![TCR Relaxed variant](./variant-relaxed.png) +![TCR Relaxed variant](../webapp/src/assets/images/variant-relaxed.png) diff --git a/variants-doc/variant-btcr.png b/webapp/src/assets/images/variant-btcr.png similarity index 100% rename from variants-doc/variant-btcr.png rename to webapp/src/assets/images/variant-btcr.png diff --git a/variants-doc/variant-original.png b/webapp/src/assets/images/variant-original.png similarity index 100% rename from variants-doc/variant-original.png rename to webapp/src/assets/images/variant-original.png diff --git a/variants-doc/variant-relaxed.png b/webapp/src/assets/images/variant-relaxed.png similarity index 100% rename from variants-doc/variant-relaxed.png rename to webapp/src/assets/images/variant-relaxed.png From c717dd7878fb002e761beaceca7594ef26ac81ba Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Tue, 10 Sep 2024 09:28:16 +0200 Subject: [PATCH 22/44] [#674] Include variant state diagram image in webapp session info page --- .../tcr-session-info.component.css | 22 ++++- .../tcr-session-info.component.html | 97 ++++++++++--------- .../tcr-session-info.component.ts | 5 +- webapp/src/assets/.gitkeep | 0 4 files changed, 73 insertions(+), 51 deletions(-) delete mode 100644 webapp/src/assets/.gitkeep diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.css b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.css index 9d525209..86bf7a70 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.css +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.css @@ -26,6 +26,12 @@ section { background-color: #f1f4fa; } +.statechart { + vertical-align: center; + width: auto; + height: auto; +} + .wrap { display: flex; background: white; @@ -36,7 +42,7 @@ section { } .ico-wrap { - margin: auto; + margin: 10px; } .mbr-iconfont { @@ -47,7 +53,7 @@ section { } .vcenter { - margin: auto; + margin: 10px; } .mbr-section-title3 { @@ -78,3 +84,15 @@ p { font-family: 'Source Sans Pro', sans-serif; font-size: 1rem; } + +.table-header { + font-family: 'Source Sans Pro', sans-serif; + font-weight: lighter; + font-size: 1rem; +} + +.table-value { + font-family: 'Source Sans Pro', sans-serif; + font-weight: bold; + font-size: 1.1rem; +} diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html index 3735d13c..59313173 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html @@ -1,85 +1,88 @@
+

{{ title }}

- -
+
- +
-

Directories

-

Base Directory

-
    -
  • {{ sessionInfo.baseDir }}
  • -
-

Work Directory

-
    -
  • {{ sessionInfo.workDir }}
  • -
+

Workflow Variant: {{ sessionInfo.variant | showEmpty }}

+ {{ sessionInfo.variant | showEmpty }}
+
-
+
+
- +
-

Language and Toolchain

-

Language

-
    -
  • {{ sessionInfo.language }}
  • -
-

Toolchain

-
    -
  • {{ sessionInfo.toolchain }}
  • -
+

Directories

+ + + + + + + + + +
Base Directory:{{ sessionInfo.baseDir }}
Work Directory:{{ sessionInfo.workDir }}
-
+
- +
-

Version Control System

-

Used VCS

-
    -
  • {{ sessionInfo.vcsName }}
  • -
-

Session Summary

-
    -
  • {{ sessionInfo.vcsSession }}
  • -
+

Language and Toolchain

+ + + + + + + + + +
Language:{{ sessionInfo.language }}
Toolchain:{{ sessionInfo.toolchain }}
-
+
- +
-

Options and Flags

-

VCS Commit Message Suffix

-
    -
  • {{ sessionInfo.messageSuffix | showEmpty }}
  • -
-

VCS - Commit-On-Fail {{ sessionInfo.commitOnFail | onOff }}

-

- Variant "{{ sessionInfo.variant | showEmpty }}"

-

Git - Auto-Push {{ sessionInfo.gitAutoPush | onOff }}

+

Version Control System

+ + + + + + + + + + + + + +
Used VCS:{{ sessionInfo.vcsName }}
Session Summary:{{ sessionInfo.vcsSession }}
Git Auto-Push:{{ sessionInfo.gitAutoPush | onOff }}
diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.ts b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.ts index 3ddd3f55..9bcb8ccf 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.ts +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.ts @@ -23,7 +23,7 @@ SOFTWARE. import {Component, Input, OnInit} from '@angular/core'; import {TcrSessionInfo} from "../../interfaces/tcr-session-info"; import {TcrSessionInfoService} from "../../services/tcr-session-info.service"; -import {DatePipe, NgIf} from "@angular/common"; +import {DatePipe, NgIf, NgOptimizedImage} from "@angular/common"; import {OnOffPipe} from "../../pipes/on-off.pipe"; import {ShowEmptyPipe} from "../../pipes/show-empty.pipe"; @@ -34,7 +34,8 @@ import {ShowEmptyPipe} from "../../pipes/show-empty.pipe"; DatePipe, NgIf, OnOffPipe, - ShowEmptyPipe + ShowEmptyPipe, + NgOptimizedImage ], templateUrl: './tcr-session-info.component.html', styleUrl: './tcr-session-info.component.css' diff --git a/webapp/src/assets/.gitkeep b/webapp/src/assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 715f2a1b36eed16c0173a484b06cb0b5c389d5b4 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Thu, 12 Sep 2024 11:11:51 +0200 Subject: [PATCH 23/44] [#674] Refactor session info page --- .../tcr-session-info.component.html | 9 +++- .../tcr-session-info.component.spec.ts | 20 ++++---- .../tcr-session-info.component.ts | 7 ++- webapp/src/app/interfaces/tcr-session-info.ts | 20 ++++++++ webapp/src/app/pipes/format-timer.pipe.ts | 3 +- webapp/src/app/pipes/on-off.pipe.ts | 2 +- webapp/src/app/pipes/show-empty.pipe.ts | 3 +- .../pipes/variant-description.pipe.spec.ts | 46 +++++++++++++++++++ .../src/app/pipes/variant-description.pipe.ts | 38 +++++++++++++++ .../app/pipes/variant-image-path.pipe.spec.ts | 44 ++++++++++++++++++ .../src/app/pipes/variant-image-path.pipe.ts | 39 ++++++++++++++++ 11 files changed, 216 insertions(+), 15 deletions(-) create mode 100644 webapp/src/app/pipes/variant-description.pipe.spec.ts create mode 100644 webapp/src/app/pipes/variant-description.pipe.ts create mode 100644 webapp/src/app/pipes/variant-image-path.pipe.spec.ts create mode 100644 webapp/src/app/pipes/variant-image-path.pipe.ts diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html index 59313173..30db14b2 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.html @@ -12,8 +12,13 @@

{{ title }}

diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts index 8fc7f196..4900f358 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts @@ -29,7 +29,7 @@ import {TcrSessionInfoComponent} from "./tcr-session-info.component"; const sample: TcrSessionInfo = { baseDir: "/my/base/dir", commitOnFail: false, - variant: "nice", + variant: "relaxed", gitAutoPush: false, language: "java", messageSuffix: "my-suffix", @@ -66,15 +66,19 @@ describe('TcrSessionInfoComponent', () => { fixture.detectChanges(); }); - it('should be created', () => { - expect(component).toBeTruthy(); - }); + describe('component instance', () => { + it('should be created', () => { + expect(component).toBeTruthy(); + }); - it('should have title "TCR Session Information"', () => { - expect(component.title).toEqual('TCR Session Information'); + it('should have title "TCR Session Information"', () => { + expect(component.title).toEqual('TCR Session Information'); + }); }); - it('should fetch TCR session info on init', () => { - expect(component.sessionInfo).toEqual(sample); + describe('component initialization', () => { + it('should fetch TCR session info on init', () => { + expect(component.sessionInfo).toEqual(sample); + }); }); }); diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.ts b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.ts index 9bcb8ccf..52b8e14d 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.ts +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.ts @@ -26,6 +26,8 @@ import {TcrSessionInfoService} from "../../services/tcr-session-info.service"; import {DatePipe, NgIf, NgOptimizedImage} from "@angular/common"; import {OnOffPipe} from "../../pipes/on-off.pipe"; import {ShowEmptyPipe} from "../../pipes/show-empty.pipe"; +import {VariantDescriptionPipe} from "../../pipes/variant-description.pipe"; +import {VariantImagePathPipe} from "../../pipes/variant-image-path.pipe"; @Component({ selector: 'app-tcr-session-info', @@ -35,7 +37,9 @@ import {ShowEmptyPipe} from "../../pipes/show-empty.pipe"; NgIf, OnOffPipe, ShowEmptyPipe, - NgOptimizedImage + NgOptimizedImage, + VariantDescriptionPipe, + VariantImagePathPipe ], templateUrl: './tcr-session-info.component.html', styleUrl: './tcr-session-info.component.css' @@ -56,4 +60,5 @@ export class TcrSessionInfoComponent implements OnInit { this.sessionInfoService.getSessionInfo() .subscribe(sessionInfo => this.sessionInfo = sessionInfo); } + } diff --git a/webapp/src/app/interfaces/tcr-session-info.ts b/webapp/src/app/interfaces/tcr-session-info.ts index 4a3e9211..00ff9545 100644 --- a/webapp/src/app/interfaces/tcr-session-info.ts +++ b/webapp/src/app/interfaces/tcr-session-info.ts @@ -32,3 +32,23 @@ export interface TcrSessionInfo { gitAutoPush: boolean; messageSuffix: string; } + +export interface TcrVariant { + description: string; + statechartImageFile: string; +} + +export const tcrVariants: { [key: string]: TcrVariant } = { + "original": { + description: "The Original", + statechartImageFile: "variant-original.png", + }, + "btcr": { + description: "BTCR -- Build && Test && Commit || Revert", + statechartImageFile: "variant-btcr.png", + }, + "relaxed": { + description: "The Relaxed", + statechartImageFile: "variant-relaxed.png", + }, +}; diff --git a/webapp/src/app/pipes/format-timer.pipe.ts b/webapp/src/app/pipes/format-timer.pipe.ts index 6dcc83d2..f081e7bf 100644 --- a/webapp/src/app/pipes/format-timer.pipe.ts +++ b/webapp/src/app/pipes/format-timer.pipe.ts @@ -31,7 +31,8 @@ const SECONDS_PER_HOUR = 3600; standalone: true }) export class FormatTimerPipe implements PipeTransform { - transform(value: unknown, ..._args: unknown[]): unknown { + + transform(value: number | string | null | undefined, ..._args: unknown[]): string { const duration: number = parseInt(value as string, 10); if (isNaN(duration)) { return `--${SEPARATOR}--`; diff --git a/webapp/src/app/pipes/on-off.pipe.ts b/webapp/src/app/pipes/on-off.pipe.ts index b5e356fe..57503105 100644 --- a/webapp/src/app/pipes/on-off.pipe.ts +++ b/webapp/src/app/pipes/on-off.pipe.ts @@ -28,7 +28,7 @@ import {Pipe, PipeTransform} from '@angular/core'; }) export class OnOffPipe implements PipeTransform { - transform(value: unknown, ..._args: unknown[]): unknown { + transform(value: boolean | string | null | undefined, ..._args: unknown[]): string { return value ? "✅" : "❌"; } diff --git a/webapp/src/app/pipes/show-empty.pipe.ts b/webapp/src/app/pipes/show-empty.pipe.ts index 03055727..1ab556b6 100644 --- a/webapp/src/app/pipes/show-empty.pipe.ts +++ b/webapp/src/app/pipes/show-empty.pipe.ts @@ -29,8 +29,7 @@ const NOT_SET: string = "[not set]"; standalone: true }) export class ShowEmptyPipe implements PipeTransform { - transform(value: unknown, ..._args: unknown[]): unknown { + transform(value: string | null | undefined, ..._args: unknown[]): string { return value || NOT_SET; } - } diff --git a/webapp/src/app/pipes/variant-description.pipe.spec.ts b/webapp/src/app/pipes/variant-description.pipe.spec.ts new file mode 100644 index 00000000..324f2f53 --- /dev/null +++ b/webapp/src/app/pipes/variant-description.pipe.spec.ts @@ -0,0 +1,46 @@ +/* +Copyright (c) 2024 Murex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import {VariantDescriptionPipe} from './variant-description.pipe'; + +describe('VariantDescriptionPipe', () => { + let pipe: VariantDescriptionPipe; + + beforeEach(() => { + pipe = new VariantDescriptionPipe(); + }); + + const notSet = '⚠️ [unknown]'; + + [ + {input: 'relaxed', expected: 'The Relaxed'}, + {input: 'btcr', expected: 'BTCR -- Build && Test && Commit || Revert'}, + {input: 'original', expected: 'The Original'}, + {input: null, expected: notSet}, + {input: undefined, expected: notSet}, + {input: '', expected: notSet} + ].forEach(testCase => { + it(`should return "${testCase.expected}" when the variant value is "${testCase.input}"`, () => { + expect(pipe.transform(testCase.input)).toEqual(testCase.expected); + }); + }); +}); diff --git a/webapp/src/app/pipes/variant-description.pipe.ts b/webapp/src/app/pipes/variant-description.pipe.ts new file mode 100644 index 00000000..cc7eb3b3 --- /dev/null +++ b/webapp/src/app/pipes/variant-description.pipe.ts @@ -0,0 +1,38 @@ +/* +Copyright (c) 2024 Murex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import {Pipe, PipeTransform} from '@angular/core'; +import {tcrVariants} from "../interfaces/tcr-session-info"; + +const UNKNOWN = "⚠️ [unknown]"; + +@Pipe({ + name: 'variantDescription', + standalone: true +}) +export class VariantDescriptionPipe implements PipeTransform { + + transform(value: string | null | undefined, ..._args: unknown[]): string { + return (value && value in tcrVariants) ? tcrVariants[value].description : UNKNOWN; + } + +} diff --git a/webapp/src/app/pipes/variant-image-path.pipe.spec.ts b/webapp/src/app/pipes/variant-image-path.pipe.spec.ts new file mode 100644 index 00000000..058045e4 --- /dev/null +++ b/webapp/src/app/pipes/variant-image-path.pipe.spec.ts @@ -0,0 +1,44 @@ +/* +Copyright (c) 2024 Murex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import {VariantImagePathPipe} from './variant-image-path.pipe'; + +describe('VariantImagePathPipe', () => { + let pipe: VariantImagePathPipe; + + beforeEach(() => { + pipe = new VariantImagePathPipe(); + }); + + [ + {input: 'relaxed', expected: 'assets/images/variant-relaxed.png'}, + {input: 'btcr', expected: 'assets/images/variant-btcr.png'}, + {input: 'original', expected: 'assets/images/variant-original.png'}, + {input: null, expected: ''}, + {input: undefined, expected: ''}, + {input: '', expected: ''} + ].forEach(testCase => { + it(`should return "${testCase.expected}" when the variant value is "${testCase.input}"`, () => { + expect(pipe.transform(testCase.input)).toEqual(testCase.expected); + }); + }); +}); diff --git a/webapp/src/app/pipes/variant-image-path.pipe.ts b/webapp/src/app/pipes/variant-image-path.pipe.ts new file mode 100644 index 00000000..50a4abed --- /dev/null +++ b/webapp/src/app/pipes/variant-image-path.pipe.ts @@ -0,0 +1,39 @@ +/* +Copyright (c) 2024 Murex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import {Pipe, PipeTransform} from '@angular/core'; +import {tcrVariants} from "../interfaces/tcr-session-info"; + +const IMAGES_PATH = "assets/images" + +@Pipe({ + name: 'variantImagePath', + standalone: true +}) +export class VariantImagePathPipe implements PipeTransform { + + transform(value: string | null | undefined, ..._args: unknown[]): string { + return (value && value in tcrVariants) + ? `${IMAGES_PATH}/${tcrVariants[value].statechartImageFile}` : ""; + } + +} From e007a02d13493da3349c8f80d06f0babe23b5998 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Fri, 13 Sep 2024 10:44:38 +0200 Subject: [PATCH 24/44] [#674] Add "introspective" variant statechart image - create variant-introspective.mmd - update other diagrams to add "VCS" on VCS-related actions - generate png files --- variants-doc/mmd/variant-btcr.mmd | 4 +-- variants-doc/mmd/variant-introspective.mmd | 24 ++++++++++++++++++ variants-doc/mmd/variant-original.mmd | 4 +-- variants-doc/mmd/variant-relaxed.mmd | 4 +-- webapp/src/assets/images/variant-btcr.png | Bin 19941 -> 21378 bytes .../assets/images/variant-introspective.png | Bin 0 -> 22822 bytes webapp/src/assets/images/variant-original.png | Bin 12855 -> 14144 bytes webapp/src/assets/images/variant-relaxed.png | Bin 19969 -> 21371 bytes 8 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 variants-doc/mmd/variant-introspective.mmd create mode 100644 webapp/src/assets/images/variant-introspective.png diff --git a/variants-doc/mmd/variant-btcr.mmd b/variants-doc/mmd/variant-btcr.mmd index 8b42b22a..28768c3a 100644 --- a/variants-doc/mmd/variant-btcr.mmd +++ b/variants-doc/mmd/variant-btcr.mmd @@ -2,8 +2,8 @@ stateDiagram-v2 direction LR state "⚙️ build" as Build state "⚙️ test" as Test - state "✅ commit (src + tests)" as Commit - state "❌ revert (src + tests)" as Revert + state "✅ VCS commit (src + tests)" as Commit + state "❌ VCS revert (src + tests)" as Revert [*] --> Build Build --> Test: pass Build --> [*]: fail diff --git a/variants-doc/mmd/variant-introspective.mmd b/variants-doc/mmd/variant-introspective.mmd new file mode 100644 index 00000000..b09b2145 --- /dev/null +++ b/variants-doc/mmd/variant-introspective.mmd @@ -0,0 +1,24 @@ +stateDiagram-v2 + direction LR + state "⚙️ build" as Build + state "⚙️ test" as Test + state "✅ VCS commit (src + tests)" as CommitPass + state "❌ VCS commit (src + tests)" as CommitFail + state "⏪ VCS revert (last commit)" as CommitRevert + + [*] --> Build + Build --> Test: pass + Build --> [*]: fail + Test --> CommitPass: pass + Test --> CommitFail: fail + CommitPass --> [*] + CommitFail --> CommitRevert + CommitRevert --> [*] + classDef actionClass fill: #0077CC + classDef okClass fill: #006600 + classDef failClass fill: #660000 + class Build actionClass + class Test actionClass + class CommitPass okClass + class CommitFail failClass + class CommitRevert actionClass diff --git a/variants-doc/mmd/variant-original.mmd b/variants-doc/mmd/variant-original.mmd index e36679ce..79161ba1 100644 --- a/variants-doc/mmd/variant-original.mmd +++ b/variants-doc/mmd/variant-original.mmd @@ -1,8 +1,8 @@ stateDiagram-v2 direction LR state "⚙️ test" as Test - state "✅ commit (src + tests)" as Commit - state "❌ revert (src + tests)" as Revert + state "✅ VCS commit (src + tests)" as Commit + state "❌ VCS revert (src + tests)" as Revert [*] --> Test Test --> Commit: pass Test --> Revert: fail diff --git a/variants-doc/mmd/variant-relaxed.mmd b/variants-doc/mmd/variant-relaxed.mmd index 7544e60b..1d6e3be8 100644 --- a/variants-doc/mmd/variant-relaxed.mmd +++ b/variants-doc/mmd/variant-relaxed.mmd @@ -2,8 +2,8 @@ stateDiagram-v2 direction LR state "⚙️ build" as Build state "⚙️ test" as Test - state "✅ commit (src + tests)" as Commit - state "❌ revert (src)" as Revert + state "✅ VCS commit (src + tests)" as Commit + state "❌ VCS revert (src)" as Revert [*] --> Build Build --> Test: pass Build --> [*]: fail diff --git a/webapp/src/assets/images/variant-btcr.png b/webapp/src/assets/images/variant-btcr.png index c96080ccbc68b4b3390d904bd82ea4be3531b07f..11b8fe96f2ac06a9bc6efe0f58590fd891c957f9 100644 GIT binary patch literal 21378 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 literal 0 HcmV?d00001 diff --git a/webapp/src/assets/images/variant-original.png b/webapp/src/assets/images/variant-original.png index 79f72fa59cd1ab20038e9ee2c19eff27abd51931..f0be058a37fb45b5e9da1798c07bb80950658b1a 100644 GIT binary patch literal 14144 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 YN3Jjz;{tW?lQ@W+wBnm`DWh-y2a{MF00000 literal 12855 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 diff --git a/webapp/src/assets/images/variant-relaxed.png b/webapp/src/assets/images/variant-relaxed.png index 26a47f90ed82a21bd5e03e926aadddfc526fee41..c59fb95405ff39ae46681241f068fb64b3a7df9e 100644 GIT binary patch literal 21371 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 S3IJ}J5MzB)y(%4-nEwJn*k%*} literal 19969 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 Vs!VH)12f$q_BI!hPptg^{1@X Date: Fri, 13 Sep 2024 10:54:54 +0200 Subject: [PATCH 25/44] [#674] Add a section about the introspective variants in doc To document what this new variant does - add a section - add a description of the variant - also add an extract from T Deniffel's post to explain what the other variants are --- variants-doc/tcr_variants.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/variants-doc/tcr_variants.md b/variants-doc/tcr_variants.md index 7f6f6aec..77ccf95f 100644 --- a/variants-doc/tcr_variants.md +++ b/variants-doc/tcr_variants.md @@ -1,7 +1,6 @@ # TCR Variants -TCR tool can run several variants of TCR, as described in -[this blog post](https://medium.com/@tdeniffel/tcr-variants-test-commit-revert-bf6bd84b17d3) +TCR tool can run several variants of TCR, inspired by [this blog post](https://medium.com/@tdeniffel/tcr-variants-test-commit-revert-bf6bd84b17d3) by Thomas Deniffel. We encourage you to read through Thomas's blog post for a deeper understanding of how these @@ -12,9 +11,10 @@ TCR's tool `--variant` (or `-r`) command-line option. The variants currently supported by TCR tool are the following: -- The Original +- The Original (not yet supported) - BTCR - The Relaxed (default) +- The introspective The state diagrams below summarize the behavior of each variant. @@ -28,6 +28,9 @@ tcr --variant=original ## BTCR - Build && Test && Commit || Revert +> This version tries to build the software first. If it fails, nothing is +> executed — no commit, no revert. Compilation-Issue solved. + ```shell tcr --variant=btcr ``` @@ -36,6 +39,9 @@ tcr --variant=btcr ## The Relaxed +> The second issue of the original suggestion is that your unit-tests get deleted all the time. +> The idea is, not to reset the ‘test’ folder but only the ‘src’ folder. + ```shell tcr --variant=relaxed ``` @@ -43,3 +49,16 @@ tcr --variant=relaxed The above option `--variant=relaxed` may be omitted as this is the default variant. ![TCR Relaxed variant](../webapp/src/assets/images/variant-relaxed.png) + +## The Introspective + +This is an extension to the BTCR variant. Instead of simply reverting changes locally, it commits the failing +changes, and straightaway commits a revert commit to get back to a green state. The goal is to be able to +retrospect on one's progress through a task. It's useful during deliberate practice sessions, but could also +serve in day to day coding. + +```shell +tcr --variant=introspective +``` + +![TCR Introspective variant](../webapp/src/assets/images/variant-introspective.png) From f1cbb056fc3047c2c859423202ba4f02a8c8f301 Mon Sep 17 00:00:00 2001 From: Philippe Bourgau Date: Fri, 13 Sep 2024 11:20:18 +0200 Subject: [PATCH 26/44] [#674] Add support for introspective Variant in UI and CMD So that users can input variant=introspective - add introspective to variant enum - make sure web app understand the introspective variant --- src/variant/variant.go | 5 +++-- src/variant/variant_test.go | 1 + webapp/src/app/interfaces/tcr-session-info.ts | 4 ++++ webapp/src/app/pipes/variant-description.pipe.spec.ts | 1 + webapp/src/app/pipes/variant-image-path.pipe.spec.ts | 1 + 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/variant/variant.go b/src/variant/variant.go index cf85130c..07c74a3e 100644 --- a/src/variant/variant.go +++ b/src/variant/variant.go @@ -31,6 +31,7 @@ func (v Variant) Name() string { } const ( - Relaxed Variant = "relaxed" - BTCR Variant = "btcr" + Relaxed Variant = "relaxed" + BTCR Variant = "btcr" + Introspective Variant = "introspective" ) diff --git a/src/variant/variant_test.go b/src/variant/variant_test.go index ce24b9b0..33c87891 100644 --- a/src/variant/variant_test.go +++ b/src/variant/variant_test.go @@ -35,6 +35,7 @@ func Test_get_variant_name(t *testing.T) { }{ {"relaxed", Relaxed, "relaxed"}, {"btcr", BTCR, "btcr"}, + {"introspective", Introspective, "introspective"}, } for _, test := range tests { diff --git a/webapp/src/app/interfaces/tcr-session-info.ts b/webapp/src/app/interfaces/tcr-session-info.ts index 00ff9545..64e187ce 100644 --- a/webapp/src/app/interfaces/tcr-session-info.ts +++ b/webapp/src/app/interfaces/tcr-session-info.ts @@ -51,4 +51,8 @@ export const tcrVariants: { [key: string]: TcrVariant } = { description: "The Relaxed", statechartImageFile: "variant-relaxed.png", }, + "introspective": { + description: "The Introspective", + statechartImageFile: "variant-introspective.png", + }, }; diff --git a/webapp/src/app/pipes/variant-description.pipe.spec.ts b/webapp/src/app/pipes/variant-description.pipe.spec.ts index 324f2f53..12ba2740 100644 --- a/webapp/src/app/pipes/variant-description.pipe.spec.ts +++ b/webapp/src/app/pipes/variant-description.pipe.spec.ts @@ -35,6 +35,7 @@ describe('VariantDescriptionPipe', () => { {input: 'relaxed', expected: 'The Relaxed'}, {input: 'btcr', expected: 'BTCR -- Build && Test && Commit || Revert'}, {input: 'original', expected: 'The Original'}, + {input: 'introspective', expected: 'The Introspective'}, {input: null, expected: notSet}, {input: undefined, expected: notSet}, {input: '', expected: notSet} diff --git a/webapp/src/app/pipes/variant-image-path.pipe.spec.ts b/webapp/src/app/pipes/variant-image-path.pipe.spec.ts index 058045e4..c396f827 100644 --- a/webapp/src/app/pipes/variant-image-path.pipe.spec.ts +++ b/webapp/src/app/pipes/variant-image-path.pipe.spec.ts @@ -33,6 +33,7 @@ describe('VariantImagePathPipe', () => { {input: 'relaxed', expected: 'assets/images/variant-relaxed.png'}, {input: 'btcr', expected: 'assets/images/variant-btcr.png'}, {input: 'original', expected: 'assets/images/variant-original.png'}, + {input: 'introspective', expected: 'assets/images/variant-introspective.png'}, {input: null, expected: ''}, {input: undefined, expected: ''}, {input: '', expected: ''} From d652e1f0abe67835f42c86acff3b5672c0ee269d Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Fri, 13 Sep 2024 12:05:53 +0200 Subject: [PATCH 27/44] [#674] Prepare test cases for introspective variant - add test placeholder for param variant value checking - add test placeholder for introspective variant execution by engine --- src/config/param_variant_test.go | 46 ++++++++++++++++++++++++++++++++ src/engine/tcr_test.go | 18 ++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/config/param_variant_test.go diff --git a/src/config/param_variant_test.go b/src/config/param_variant_test.go new file mode 100644 index 00000000..98bfa2f9 --- /dev/null +++ b/src/config/param_variant_test.go @@ -0,0 +1,46 @@ +/* +Copyright (c) 2024 Murex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package config + +import "testing" + +func Test_valid_variant_values(t *testing.T) { + t.Skip("work in progress") + tests := []struct { + input string + expectedValue string + expectedError error + }{ + { + input: "relaxed", + expectedValue: "relaxed", + expectedError: nil, + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + // TODO + }) + } +} diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index e994cfca..929efbb8 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -296,7 +296,7 @@ func Test_relaxed_doesnt_revert_test_files(t *testing.T) { assert.NotEqual(t, fake.RestoreCommand, vcsFake.GetLastCommand()) } -func Test_relaxed_reverts(t *testing.T) { +func Test_variant_specific_reverts(t *testing.T) { testFlags := []struct { description string variant variant.Variant @@ -360,6 +360,22 @@ func Test_relaxed_reverts(t *testing.T) { } } +func Test_introspective_variant(t *testing.T) { + t.Skip("work in progress") + tcr, vcsFake := initTCREngineWithFakesWithFileDiffs( + params.AParamSet(params.WithVariant(variant.Introspective)), + nil, nil, nil, vcs.FileDiffs{ + vcs.NewFileDiff("fake-src", 1, 1), + vcs.NewFileDiff("fake-test", 1, 1), + }) + + tcr.revert(*events.ATcrEvent()) + //sniffer.Stop() + assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) + //assert.Equal(t, 1, sniffer.GetMatchCount()) + +} + func Test_tcr_cycle_end_state(t *testing.T) { testFlags := []struct { desc string From 2c6b572663cb5f3f391e4d3e7dbf43db6e8b6d59 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Mon, 16 Sep 2024 18:37:34 +0200 Subject: [PATCH 28/44] [#674] Add handling of variant parameter authorized values - change param.variant type to string to be consistant with other params handling - remove config/param_variant_test.go - add variant.Select(string) to manage variant instance selection - add UnsupportedVariantError (returned when variant name is not recognized) - ensure TCR engine exits on error when variant is not recognized --- src/config/param_variant_test.go | 46 -------------------------- src/config/tcr_config.go | 3 +- src/engine/tcr.go | 26 ++++++++++----- src/engine/tcr_test.go | 6 ++-- src/params/params.go | 3 +- src/params/params_test_data_builder.go | 5 ++- src/variant/variant.go | 39 +++++++++++++++++++--- src/variant/variant_test.go | 27 +++++++++++++++ 8 files changed, 85 insertions(+), 70 deletions(-) delete mode 100644 src/config/param_variant_test.go diff --git a/src/config/param_variant_test.go b/src/config/param_variant_test.go deleted file mode 100644 index 98bfa2f9..00000000 --- a/src/config/param_variant_test.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright (c) 2024 Murex - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -package config - -import "testing" - -func Test_valid_variant_values(t *testing.T) { - t.Skip("work in progress") - tests := []struct { - input string - expectedValue string - expectedError error - }{ - { - input: "relaxed", - expectedValue: "relaxed", - expectedError: nil, - }, - } - - for _, test := range tests { - t.Run(test.input, func(t *testing.T) { - // TODO - }) - } -} diff --git a/src/config/tcr_config.go b/src/config/tcr_config.go index 1ebc45b9..185eb4d2 100644 --- a/src/config/tcr_config.go +++ b/src/config/tcr_config.go @@ -28,7 +28,6 @@ import ( "github.com/murex/tcr/settings" "github.com/murex/tcr/toolchain" "github.com/murex/tcr/utils" - "github.com/murex/tcr/variant" "github.com/spf13/cobra" "github.com/spf13/viper" "io" @@ -221,7 +220,7 @@ func UpdateEngineParams(p *params.Params) { p.Toolchain = Config.Toolchain.GetValue() p.PollingPeriod = Config.PollingPeriod.GetValue() p.AutoPush = Config.AutoPush.GetValue() - p.Variant = variant.Variant(Config.Variant.GetValue()) + p.Variant = Config.Variant.GetValue() p.CommitFailures = Config.CommitFailures.GetValue() p.VCS = Config.VCS.GetValue() p.MessageSuffix = Config.MessageSuffix.GetValue() diff --git a/src/engine/tcr.go b/src/engine/tcr.go index fce84f66..fd52498e 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -59,7 +59,7 @@ type ( ToggleAutoPush() SetAutoPush(flag bool) SetCommitOnFail(flag bool) - SetVariant(variant variant.Variant) + SetVariant(variant string) GetCurrentRole() role.Role RunAsDriver() RunAsNavigator() @@ -94,7 +94,7 @@ type ( // before starting a new one roleMutex sync.Mutex commitOnFail bool - variant variant.Variant + variant *variant.Variant messageSuffix string // shoot channel is used for handling interruptions coming from the UI shoot chan bool @@ -109,10 +109,6 @@ type ( } ) -func (tcr *TCREngine) SetVariant(variant variant.Variant) { - tcr.variant = variant -} - const traceReporterWaitingTime = 100 * time.Millisecond const fsWatchRearmDelay = 100 * time.Millisecond @@ -200,6 +196,18 @@ func (tcr *TCREngine) SetCommitOnFail(flag bool) { } } +// SetVariant sets the TCR variant that will be used by TCR engine +func (tcr *TCREngine) SetVariant(variantName string) { + var err error + tcr.variant, err = variant.Select(variantName) + if err != nil { + var unsupportedVariantError *variant.UnsupportedVariantError + if errors.As(err, &unsupportedVariantError) { + tcr.handleError(err, true, status.ConfigError) + } + } +} + func (tcr *TCREngine) setMobTimerDuration(duration time.Duration) { if settings.EnableMobTimer { if tcr.mode.IsMultiRole() { @@ -633,14 +641,14 @@ func (tcr *TCREngine) revert(event events.TCREvent) { } func (tcr *TCREngine) noFilesRevertedMessage() string { - if tcr.variant == variant.Relaxed { + if *tcr.variant == variant.Relaxed { return "No file reverted (only test files were updated since last commit)" } return "No file reverted" } func (tcr *TCREngine) shouldRevertFile(path string) bool { - return tcr.variant == variant.BTCR || tcr.language.IsSrcFile(path) + return *tcr.variant == variant.BTCR || tcr.language.IsSrcFile(path) } func (tcr *TCREngine) commitTestBreakingChanges(event events.TCREvent) (err error) { @@ -694,7 +702,7 @@ func (tcr *TCREngine) GetSessionInfo() SessionInfo { VCSSessionSummary: tcr.vcs.SessionSummary(), GitAutoPush: tcr.vcs.IsAutoPushEnabled(), CommitOnFail: tcr.commitOnFail, - Variant: string(tcr.variant), + Variant: tcr.variant.Name(), MessageSuffix: tcr.messageSuffix, } } diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 929efbb8..f49bad62 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -350,7 +350,7 @@ func Test_variant_specific_reverts(t *testing.T) { }, ) tcr, vcsFake := initTCREngineWithFakesWithFileDiffs( - params.AParamSet(params.WithVariant(tt.variant)), + params.AParamSet(params.WithVariant(tt.variant.Name())), nil, nil, nil, tt.fileDiffs) tcr.revert(*events.ATcrEvent()) sniffer.Stop() @@ -363,7 +363,7 @@ func Test_variant_specific_reverts(t *testing.T) { func Test_introspective_variant(t *testing.T) { t.Skip("work in progress") tcr, vcsFake := initTCREngineWithFakesWithFileDiffs( - params.AParamSet(params.WithVariant(variant.Introspective)), + params.AParamSet(params.WithVariant(variant.Introspective.Name())), nil, nil, nil, vcs.FileDiffs{ vcs.NewFileDiff("fake-src", 1, 1), vcs.NewFileDiff("fake-test", 1, 1), @@ -589,7 +589,7 @@ func Test_set_variant(t *testing.T) { for _, tt := range testFlags { t.Run(fmt.Sprintf("Variant %v", tt.variant), func(t *testing.T) { tcr, _ = initTCREngineWithFakes(nil, nil, nil, nil) - tcr.SetVariant(tt.variant) + tcr.SetVariant(tt.variant.Name()) assert.Equal(t, string(tt.variant), tcr.GetSessionInfo().Variant) }) } diff --git a/src/params/params.go b/src/params/params.go index 16533e95..55b9f85d 100644 --- a/src/params/params.go +++ b/src/params/params.go @@ -24,7 +24,6 @@ package params import ( "github.com/murex/tcr/runmode" - "github.com/murex/tcr/variant" "time" ) @@ -37,7 +36,7 @@ type Params struct { Toolchain string MobTurnDuration time.Duration AutoPush bool - Variant variant.Variant + Variant string CommitFailures bool PollingPeriod time.Duration Mode runmode.RunMode diff --git a/src/params/params_test_data_builder.go b/src/params/params_test_data_builder.go index 862c20b9..162ed620 100644 --- a/src/params/params_test_data_builder.go +++ b/src/params/params_test_data_builder.go @@ -26,7 +26,6 @@ package params import ( "github.com/murex/tcr/runmode" - "github.com/murex/tcr/variant" "time" ) @@ -40,7 +39,7 @@ func AParamSet(builders ...func(params *Params)) *Params { Toolchain: "", MobTurnDuration: 0, AutoPush: false, - Variant: variant.Relaxed, + Variant: "relaxed", PollingPeriod: 0, Mode: runmode.OneShot{}, VCS: "git", @@ -110,7 +109,7 @@ func WithAutoPush(value bool) func(params *Params) { } // WithVariant sets the provided value as the variant to be used -func WithVariant(variant variant.Variant) func(params *Params) { +func WithVariant(variant string) func(params *Params) { return func(params *Params) { params.Variant = variant } diff --git a/src/variant/variant.go b/src/variant/variant.go index 07c74a3e..8fd189df 100644 --- a/src/variant/variant.go +++ b/src/variant/variant.go @@ -22,16 +22,45 @@ SOFTWARE. package variant -// Variant represents the possible values for the TCR Variant -// https://medium.com/@tdeniffel/tcr-variants-test-commit-revert-bf6bd84b17d3 -type Variant string +import "fmt" -func (v Variant) Name() string { - return string(v) +// UnsupportedVariantError is returned when the provided Variant name is not supported. +type UnsupportedVariantError struct { + variantName string } +// Error returns the error description +func (e *UnsupportedVariantError) Error() string { + return fmt.Sprintf("variant not supported: \"%s\"", e.variantName) +} + +// Variant represents the possible values for the TCR Variant. +// These values are inspired by the following blog-post: +// https://medium.com/@tdeniffel/tcr-variants-test-commit-revert-bf6bd84b17d3 +type Variant string + +// Recognized variant values const ( Relaxed Variant = "relaxed" BTCR Variant = "btcr" Introspective Variant = "introspective" ) + +var recognized = []Variant{Relaxed, BTCR, Introspective} + +// Select returns a variant instance for the provided name. +// It returns an UnsupportedVariantError if the name is not recognized as a +// valid variant name. +func Select(name string) (*Variant, error) { + for _, variant := range recognized { + if name == variant.Name() { + return &variant, nil + } + } + return nil, &UnsupportedVariantError{name} +} + +// Name returns the variant name +func (v Variant) Name() string { + return string(v) +} diff --git a/src/variant/variant_test.go b/src/variant/variant_test.go index 33c87891..1cbc29f8 100644 --- a/src/variant/variant_test.go +++ b/src/variant/variant_test.go @@ -44,3 +44,30 @@ func Test_get_variant_name(t *testing.T) { }) } } + +func Test_select_variant(t *testing.T) { + relaxed, btcr, introspective := Relaxed, BTCR, Introspective + tests := []struct { + name string + expectedVariant *Variant + expectedError error + }{ + {"relaxed", &relaxed, nil}, + {"btcr", &btcr, nil}, + {"introspective", &introspective, nil}, + {"unknown", nil, &UnsupportedVariantError{"unknown"}}, + {"", nil, &UnsupportedVariantError{""}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + variant, err := Select(test.name) + assert.Equal(t, test.expectedVariant, variant) + assert.Equal(t, test.expectedError, err) + }) + } +} + +func Test_unsupported_variant_message_format(t *testing.T) { + err := UnsupportedVariantError{"some-variant"} + assert.Equal(t, "variant not supported: \"some-variant\"", err.Error()) +} From 98362759b38f5c7ba7184071325c9acef5323b57 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Mon, 16 Sep 2024 18:50:24 +0200 Subject: [PATCH 29/44] [#674] Fix linter errors --- src/engine/tcr.go | 6 +++--- src/engine/tcr_test.go | 1 - src/language/file_tree_filter.go | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/engine/tcr.go b/src/engine/tcr.go index fd52498e..3897497d 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -59,7 +59,7 @@ type ( ToggleAutoPush() SetAutoPush(flag bool) SetCommitOnFail(flag bool) - SetVariant(variant string) + SetVariant(name string) GetCurrentRole() role.Role RunAsDriver() RunAsNavigator() @@ -197,9 +197,9 @@ func (tcr *TCREngine) SetCommitOnFail(flag bool) { } // SetVariant sets the TCR variant that will be used by TCR engine -func (tcr *TCREngine) SetVariant(variantName string) { +func (tcr *TCREngine) SetVariant(name string) { var err error - tcr.variant, err = variant.Select(variantName) + tcr.variant, err = variant.Select(name) if err != nil { var unsupportedVariantError *variant.UnsupportedVariantError if errors.As(err, &unsupportedVariantError) { diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index f49bad62..238e125a 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -489,7 +489,6 @@ func initTCREngineWithFakesWithFileDiffs( tcr.fsWatchRearmDelay = 0 tcr.traceReporterWaitingTime = 0 return tcr, vcsFake - } func initTCREngineWithFakes( diff --git a/src/language/file_tree_filter.go b/src/language/file_tree_filter.go index 3a74bab2..ae753502 100644 --- a/src/language/file_tree_filter.go +++ b/src/language/file_tree_filter.go @@ -83,7 +83,7 @@ func toSlashedPath(input string) string { func (ftf FileTreeFilter) isInFileTree(aPath string, baseDir string) bool { absPath, _ := filepath.Abs(aPath) // If no directory is configured, any path that is under baseDir path is ok - if ftf.Directories == nil || len(ftf.Directories) == 0 { + if len(ftf.Directories) == 0 { if utils.IsSubPathOf(absPath, baseDir) { return true } @@ -104,7 +104,7 @@ func (ftf FileTreeFilter) matches(p string, baseDir string) bool { } if ftf.isInFileTree(p, baseDir) { // If no pattern is set, any file matches as long as it's in the file tree - if ftf.FilePatterns == nil || len(ftf.FilePatterns) == 0 { + if len(ftf.FilePatterns) == 0 { return true } for _, filter := range ftf.FilePatterns { From 863aec9f3d7c5047141bc6b679b6e0192189da83 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Tue, 17 Sep 2024 13:29:06 +0200 Subject: [PATCH 30/44] [#674] Continue working on test case for introspective variant - add a list of received commands in VCS fake so that we can test that we got "commit && revert" - add vcsfake.GetLastCommands(n) to get the tail of the list of last commands - add vcsfake.VerifyLastCommandsAre() as an alternative approach --- src/engine/tcr_test.go | 7 ++++--- src/vcs/fake/vcs_test_fake.go | 24 ++++++++++++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 238e125a..599330a9 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -370,9 +370,10 @@ func Test_introspective_variant(t *testing.T) { }) tcr.revert(*events.ATcrEvent()) - //sniffer.Stop() - assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) - //assert.Equal(t, 1, sniffer.GetMatchCount()) + assert.Equal(t, []fake.Command{fake.CommitCommand, fake.RevertCommand}, vcsFake.GetLastCommands(2)) + + //assert.True(t, vcsFake.VerifyLastCommandsAre(fake.CommitCommand, fake.RevertCommand), + // "got %v", vcsFake.GetLastCommands()) } diff --git a/src/vcs/fake/vcs_test_fake.go b/src/vcs/fake/vcs_test_fake.go index d14d6e62..8fb2787d 100644 --- a/src/vcs/fake/vcs_test_fake.go +++ b/src/vcs/fake/vcs_test_fake.go @@ -26,6 +26,7 @@ package fake import ( "errors" + "github.com/google/go-cmp/cmp" "github.com/murex/tcr/vcs" ) @@ -75,14 +76,14 @@ type ( // VCSFake provides a fake implementation of the VCS interface VCSFake struct { - settings Settings - pushEnabled bool - lastCommand Command + settings Settings + pushEnabled bool + lastCommands []Command } ) func (vf *VCSFake) fakeCommand(cmd Command) (err error) { - vf.lastCommand = cmd + vf.lastCommands = append(vf.lastCommands, cmd) if vf.settings.FailingCommands.contains(cmd) { err = errors.New(vf.Name() + " " + string(cmd) + " error") } @@ -92,7 +93,7 @@ func (vf *VCSFake) fakeCommand(cmd Command) (err error) { // NewVCSFake initializes a fake VCS implementation which does nothing // apart from emulating errors on VCS operations func NewVCSFake(settings Settings) *VCSFake { - return &VCSFake{settings: settings} + return &VCSFake{settings: settings, lastCommands: make([]Command, 0)} } // Name returns VCS name @@ -107,7 +108,18 @@ func (vf *VCSFake) SessionSummary() string { // GetLastCommand returns the last command called func (vf *VCSFake) GetLastCommand() Command { - return vf.lastCommand + return vf.GetLastCommands(1)[0] +} + +// GetLastCommands returns the last commands called +func (vf *VCSFake) GetLastCommands(count int) []Command { + return vf.lastCommands[len(vf.lastCommands)-count:] +} + +// VerifyLastCommandsAre checks if last executed commands are the provided ones +func (vf *VCSFake) VerifyLastCommandsAre(expectedCommands ...Command) bool { + actual := vf.lastCommands[len(vf.lastCommands)-len(expectedCommands):] + return cmp.Equal(actual, expectedCommands) } // Add does nothing. Returns an error if in the list of failing commands From 121c25ff122f8f5dd29b35f0f3774ea9a5c92929 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Wed, 18 Sep 2024 10:49:45 +0200 Subject: [PATCH 31/44] [#674] Clean up test case for introspective variant - remove vcsfake.VerifyLastCommandsAre() alternative approach --- src/engine/tcr_test.go | 4 ---- src/vcs/fake/vcs_test_fake.go | 7 ------- 2 files changed, 11 deletions(-) diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 599330a9..7704cb37 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -371,10 +371,6 @@ func Test_introspective_variant(t *testing.T) { tcr.revert(*events.ATcrEvent()) assert.Equal(t, []fake.Command{fake.CommitCommand, fake.RevertCommand}, vcsFake.GetLastCommands(2)) - - //assert.True(t, vcsFake.VerifyLastCommandsAre(fake.CommitCommand, fake.RevertCommand), - // "got %v", vcsFake.GetLastCommands()) - } func Test_tcr_cycle_end_state(t *testing.T) { diff --git a/src/vcs/fake/vcs_test_fake.go b/src/vcs/fake/vcs_test_fake.go index 8fb2787d..73c1c284 100644 --- a/src/vcs/fake/vcs_test_fake.go +++ b/src/vcs/fake/vcs_test_fake.go @@ -26,7 +26,6 @@ package fake import ( "errors" - "github.com/google/go-cmp/cmp" "github.com/murex/tcr/vcs" ) @@ -116,12 +115,6 @@ func (vf *VCSFake) GetLastCommands(count int) []Command { return vf.lastCommands[len(vf.lastCommands)-count:] } -// VerifyLastCommandsAre checks if last executed commands are the provided ones -func (vf *VCSFake) VerifyLastCommandsAre(expectedCommands ...Command) bool { - actual := vf.lastCommands[len(vf.lastCommands)-len(expectedCommands):] - return cmp.Equal(actual, expectedCommands) -} - // Add does nothing. Returns an error if in the list of failing commands func (vf *VCSFake) Add(_ ...string) error { return vf.fakeCommand(AddCommand) From 608f699d5e44e381a5b057038a1ace2412f047c8 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Wed, 18 Sep 2024 12:00:33 +0200 Subject: [PATCH 32/44] [#674] Remote commit failure option and all related code --- .run/go build github.com_murex_tcr.run.xml | 12 ---- src/checker/check_workflow.go | 14 ++-- src/checker/check_workflow_test.go | 29 -------- src/config/param_commit_failures.go | 52 -------------- src/config/tcr_config.go | 4 -- src/config/tcr_config_test.go | 1 - src/engine/session_info.go | 1 - src/engine/tcr.go | 60 +--------------- src/engine/tcr_test.go | 68 ------------------- src/engine/tcr_test_fake.go | 1 - src/http/api/session_info.go | 1 - src/http/api/session_info_test.go | 1 - src/params/params.go | 1 - src/params/params_test_data_builder.go | 7 -- .../tcr-session-info.component.spec.ts | 1 - webapp/src/app/interfaces/tcr-session-info.ts | 1 - .../services/tcr-session-info.service.spec.ts | 1 - 17 files changed, 5 insertions(+), 250 deletions(-) delete mode 100644 .run/go build github.com_murex_tcr.run.xml delete mode 100644 src/config/param_commit_failures.go diff --git a/.run/go build github.com_murex_tcr.run.xml b/.run/go build github.com_murex_tcr.run.xml deleted file mode 100644 index 7e7dd165..00000000 --- a/.run/go build github.com_murex_tcr.run.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/checker/check_workflow.go b/src/checker/check_workflow.go index 316156d5..325c6e89 100644 --- a/src/checker/check_workflow.go +++ b/src/checker/check_workflow.go @@ -31,7 +31,7 @@ var checkWorkflowRunners []checkPointRunner func init() { checkWorkflowRunners = []checkPointRunner{ - checkCommitFailures, + checkVariant, } } @@ -43,14 +43,8 @@ func checkWorkflowConfiguration(p params.Params) (cg *model.CheckGroup) { return cg } -func checkCommitFailures(p params.Params) (cp []model.CheckPoint) { - switch p.CommitFailures { - case true: - cp = append(cp, model.OkCheckPoint( - "commit-failures is turned on: test-breaking changes will be committed")) - case false: - cp = append(cp, model.OkCheckPoint( - "commit-failures is turned off: test-breaking changes will not be committed")) - } +func checkVariant(_ params.Params) (cp []model.CheckPoint) { + cp = append(cp, model.WarningCheckPoint( + "variant checker TODO")) return cp } diff --git a/src/checker/check_workflow_test.go b/src/checker/check_workflow_test.go index 3b871c1a..5d7d275f 100644 --- a/src/checker/check_workflow_test.go +++ b/src/checker/check_workflow_test.go @@ -23,9 +23,7 @@ SOFTWARE. package checker import ( - "github.com/murex/tcr/checker/model" "github.com/murex/tcr/params" - "github.com/stretchr/testify/assert" "testing" ) @@ -36,30 +34,3 @@ func Test_check_workflow_configuration(t *testing.T) { *params.AParamSet(), "TCR workflow configuration") } - -func Test_check_commit_failures(t *testing.T) { - tests := []struct { - desc string - value bool - expected []model.CheckPoint - }{ - { - "turned off", false, - []model.CheckPoint{ - model.OkCheckPoint("commit-failures is turned off: test-breaking changes will not be committed"), - }, - }, - { - "turned on", true, - []model.CheckPoint{ - model.OkCheckPoint("commit-failures is turned on: test-breaking changes will be committed"), - }, - }, - } - for _, test := range tests { - t.Run(test.desc, func(t *testing.T) { - p := *params.AParamSet(params.WithCommitFailures(test.value)) - assert.Equal(t, test.expected, checkCommitFailures(p)) - }) - } -} diff --git a/src/config/param_commit_failures.go b/src/config/param_commit_failures.go deleted file mode 100644 index f994960a..00000000 --- a/src/config/param_commit_failures.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright (c) 2021 Murex - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -package config - -import ( - "github.com/spf13/cobra" -) - -// AddCommitFailuresParam adds VCS commit failure parameter to the provided command -func AddCommitFailuresParam(cmd *cobra.Command) *BoolParam { - param := BoolParam{ - s: paramSettings{ - viperSettings: viperSettings{ - enabled: true, - keyPath: "config.git", - name: "commit-failures", - }, - cobraSettings: cobraSettings{ - name: "commit-failures", - shorthand: "F", - usage: "enable committing reverts on tests failure", - persistent: true, - }, - }, - v: paramValueBool{ - value: false, - defaultValue: false, - }, - } - param.addToCommand(cmd) - return ¶m -} diff --git a/src/config/tcr_config.go b/src/config/tcr_config.go index 185eb4d2..3d966e6a 100644 --- a/src/config/tcr_config.go +++ b/src/config/tcr_config.go @@ -47,7 +47,6 @@ type TcrConfig struct { MobTimerDuration *DurationParam AutoPush *BoolParam Variant *StringParam - CommitFailures *BoolParam VCS *StringParam MessageSuffix *StringParam Trace *StringParam @@ -61,7 +60,6 @@ func (c TcrConfig) reset() { c.MobTimerDuration.reset() c.AutoPush.reset() c.Variant.reset() - c.CommitFailures.reset() c.VCS.reset() c.MessageSuffix.reset() c.Trace.reset() @@ -203,7 +201,6 @@ func AddParameters(cmd *cobra.Command, defaultDir string) { Config.MobTimerDuration = AddMobTimerDurationParam(cmd) Config.AutoPush = AddAutoPushParam(cmd) Config.Variant = AddVariantParam(cmd) - Config.CommitFailures = AddCommitFailuresParam(cmd) Config.VCS = AddVCSParam(cmd) Config.MessageSuffix = AddMessageSuffixParam(cmd) Config.Trace = AddTraceParam(cmd) @@ -221,7 +218,6 @@ func UpdateEngineParams(p *params.Params) { p.PollingPeriod = Config.PollingPeriod.GetValue() p.AutoPush = Config.AutoPush.GetValue() p.Variant = Config.Variant.GetValue() - p.CommitFailures = Config.CommitFailures.GetValue() p.VCS = Config.VCS.GetValue() p.MessageSuffix = Config.MessageSuffix.GetValue() p.Trace = Config.Trace.GetValue() diff --git a/src/config/tcr_config_test.go b/src/config/tcr_config_test.go index 6d9cedbe..98764950 100644 --- a/src/config/tcr_config_test.go +++ b/src/config/tcr_config_test.go @@ -239,7 +239,6 @@ func Test_show_tcr_config_with_default_values(t *testing.T) { expected := []string{ "TCR configuration:", fmt.Sprintf("%v.git.auto-push: %v", prefix, false), - fmt.Sprintf("%v.git.commit-failures: %v", prefix, false), fmt.Sprintf("%v.git.polling-period: %v", prefix, 2*time.Second), fmt.Sprintf("%v.mob-timer.duration: %v", prefix, 5*time.Minute), fmt.Sprintf("%v.tcr.language: %v", prefix, ""), diff --git a/src/engine/session_info.go b/src/engine/session_info.go index fcfa525d..7a302b0e 100644 --- a/src/engine/session_info.go +++ b/src/engine/session_info.go @@ -30,7 +30,6 @@ type SessionInfo struct { ToolchainName string VCSName string VCSSessionSummary string - CommitOnFail bool Variant string GitAutoPush bool MessageSuffix string diff --git a/src/engine/tcr.go b/src/engine/tcr.go index 3897497d..101a2534 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -58,7 +58,6 @@ type ( setVCS(vcsInterface vcs.Interface) ToggleAutoPush() SetAutoPush(flag bool) - SetCommitOnFail(flag bool) SetVariant(name string) GetCurrentRole() role.Role RunAsDriver() @@ -93,7 +92,6 @@ type ( // roles simultaneously: we wait for it to leave the previous role // before starting a new one roleMutex sync.Mutex - commitOnFail bool variant *variant.Variant messageSuffix string // shoot channel is used for handling interruptions coming from the UI @@ -177,7 +175,6 @@ func (tcr *TCREngine) Init(p params.Params) { tcr.setMessageSuffix(p.MessageSuffix) tcr.vcs.EnableAutoPush(p.AutoPush) - tcr.SetCommitOnFail(p.CommitFailures) tcr.SetVariant(p.Variant) tcr.setMobTimerDuration(p.MobTurnDuration) @@ -186,16 +183,6 @@ func (tcr *TCREngine) Init(p params.Params) { tcr.warnIfOnRootBranch(tcr.mode.IsInteractive()) } -// SetCommitOnFail sets VCS commit-on-fail option to the provided value -func (tcr *TCREngine) SetCommitOnFail(flag bool) { - tcr.commitOnFail = flag - if tcr.commitOnFail { - report.PostInfo("Test-breaking changes will be committed") - } else { - report.PostInfo("Test-breaking changes will not be committed") - } -} - // SetVariant sets the TCR variant that will be used by TCR engine func (tcr *TCREngine) SetVariant(name string) { var err error @@ -608,16 +595,7 @@ func (tcr *TCREngine) commit(event events.TCREvent) { tcr.handleError(tcr.vcsPushAuto(), false, status.VCSError) } -func (tcr *TCREngine) revert(event events.TCREvent) { - if tcr.commitOnFail { - err := tcr.commitTestBreakingChanges(event) - tcr.handleError(err, false, status.VCSError) - if err != nil { - return - } - tcr.handleError(tcr.vcsPushAuto(), false, status.VCSError) - } - +func (tcr *TCREngine) revert(_ events.TCREvent) { diffs, err := tcr.vcs.Diff() tcr.handleError(err, false, status.VCSError) if err != nil { @@ -651,41 +629,6 @@ func (tcr *TCREngine) shouldRevertFile(path string) bool { return *tcr.variant == variant.BTCR || tcr.language.IsSrcFile(path) } -func (tcr *TCREngine) commitTestBreakingChanges(event events.TCREvent) (err error) { - // Create stash with the changes - err = tcr.vcs.Stash(commitMessageFail) - if err != nil { - return err - } - // Apply changes back in the working tree - err = tcr.vcs.UnStash(true) - if err != nil { - return err - } - // Commit changes with failure message into VCS index - err = tcr.vcs.Add() - if err != nil { - return err - } - err = tcr.vcs.Commit(false, tcr.wrapCommitMessages(commitMessageFail, &event)...) - if err != nil { - return err - } - // Revert changes (both in VCS index and working tree) - err = tcr.vcs.Revert() - if err != nil { - return err - } - // Amend commit message on revert operation in VCS index - err = tcr.vcs.Commit(false, tcr.wrapCommitMessages(commitMessageRevert, nil)...) - if err != nil { - return err - } - // Re-apply changes in the working tree and get rid of stash - err = tcr.vcs.UnStash(false) - return err -} - func (tcr *TCREngine) revertFile(file string) error { return tcr.vcs.Restore(file) } @@ -701,7 +644,6 @@ func (tcr *TCREngine) GetSessionInfo() SessionInfo { VCSName: tcr.vcs.Name(), VCSSessionSummary: tcr.vcs.SessionSummary(), GitAutoPush: tcr.vcs.IsAutoPushEnabled(), - CommitOnFail: tcr.commitOnFail, Variant: tcr.variant.Name(), MessageSuffix: tcr.messageSuffix, } diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 7704cb37..e2f3573f 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -232,55 +232,6 @@ func Test_tcr_operation_end_state(t *testing.T) { } } -func Test_tcr_revert_end_state_with_commit_on_fail_enabled(t *testing.T) { - testFlags := []struct { - desc string - vcsFailures fake.Commands - expectedStatus status.Status - }{ - { - "no failure", - nil, - status.Ok, - }, - { - "VCS stash failure", - fake.Commands{fake.StashCommand}, - status.VCSError, - }, - { - "VCS un-stash failure", - fake.Commands{fake.UnStashCommand}, - status.VCSError, - }, - { - "VCS add failure", - fake.Commands{fake.AddCommand}, - status.VCSError, - }, - { - "VCS commit failure", - fake.Commands{fake.CommitCommand}, - status.VCSError, - }, - { - "VCS revert failure", - fake.Commands{fake.RevertCommand}, - status.VCSError, - }, - } - - for _, tt := range testFlags { - t.Run(tt.desc, func(t *testing.T) { - status.RecordState(status.Ok) - tcr, _ := initTCREngineWithFakes(nil, nil, tt.vcsFailures, nil) - tcr.SetCommitOnFail(true) - tcr.revert(*events.ATcrEvent()) - assert.Equal(t, tt.expectedStatus, status.GetCurrentState()) - }) - } -} - func Test_relaxed_source_revert(t *testing.T) { tcr, vcsFake := initTCREngineWithFakes(nil, nil, nil, nil) tcr.revert(*events.ATcrEvent()) @@ -459,7 +410,6 @@ func initTCREngineWithFakesWithFileDiffs( params.WithToolchain(tchn), params.WithMobTimerDuration(p.MobTurnDuration), params.WithAutoPush(p.AutoPush), - params.WithCommitFailures(p.CommitFailures), params.WithVariant(p.Variant), params.WithPollingPeriod(p.PollingPeriod), params.WithRunMode(p.Mode), @@ -556,24 +506,6 @@ func Test_set_auto_push(t *testing.T) { } } -func Test_set_commit_on_fail(t *testing.T) { - var tcr TCRInterface - testFlags := []struct { - desc string - state bool - }{ - {"Turn on", true}, - {"Turn off", false}, - } - for _, tt := range testFlags { - t.Run(tt.desc, func(t *testing.T) { - tcr, _ = initTCREngineWithFakes(nil, nil, nil, nil) - tcr.SetCommitOnFail(tt.state) - assert.Equal(t, tt.state, tcr.GetSessionInfo().CommitOnFail) - }) - } -} - func Test_set_variant(t *testing.T) { var tcr TCRInterface testFlags := []struct { diff --git a/src/engine/tcr_test_fake.go b/src/engine/tcr_test_fake.go index 174cdc48..1ca7a05e 100644 --- a/src/engine/tcr_test_fake.go +++ b/src/engine/tcr_test_fake.go @@ -79,7 +79,6 @@ func NewFakeTCREngine() *FakeTCREngine { VCSName: "fake", VCSSessionSummary: "VCS session \"fake\"", GitAutoPush: false, - CommitOnFail: false, Variant: variant.Relaxed.Name(), }, } diff --git a/src/http/api/session_info.go b/src/http/api/session_info.go index 2b782838..c3320709 100644 --- a/src/http/api/session_info.go +++ b/src/http/api/session_info.go @@ -51,7 +51,6 @@ func SessionInfoGetHandler(c *gin.Context) { ToolchainName: info.ToolchainName, VCSName: info.VCSName, VCSSessionSummary: info.VCSSessionSummary, - CommitOnFail: info.CommitOnFail, Variant: info.Variant, GitAutoPush: info.GitAutoPush, MessageSuffix: info.MessageSuffix, diff --git a/src/http/api/session_info_test.go b/src/http/api/session_info_test.go index 1091a161..9fb20a63 100644 --- a/src/http/api/session_info_test.go +++ b/src/http/api/session_info_test.go @@ -56,7 +56,6 @@ func Test_session_info_get_handler(t *testing.T) { ToolchainName: info.ToolchainName, VCSName: info.VCSName, VCSSessionSummary: info.VCSSessionSummary, - CommitOnFail: info.CommitOnFail, Variant: info.Variant, GitAutoPush: info.GitAutoPush, MessageSuffix: info.MessageSuffix, diff --git a/src/params/params.go b/src/params/params.go index 55b9f85d..ba32c922 100644 --- a/src/params/params.go +++ b/src/params/params.go @@ -37,7 +37,6 @@ type Params struct { MobTurnDuration time.Duration AutoPush bool Variant string - CommitFailures bool PollingPeriod time.Duration Mode runmode.RunMode VCS string diff --git a/src/params/params_test_data_builder.go b/src/params/params_test_data_builder.go index 162ed620..5fe0f28b 100644 --- a/src/params/params_test_data_builder.go +++ b/src/params/params_test_data_builder.go @@ -115,13 +115,6 @@ func WithVariant(variant string) func(params *Params) { } } -// WithCommitFailures sets commit-failures flag to the provided value -func WithCommitFailures(value bool) func(params *Params) { - return func(params *Params) { - params.CommitFailures = value - } -} - // WithRunMode sets the provided mode as the run mode func WithRunMode(mode runmode.RunMode) func(params *Params) { return func(params *Params) { diff --git a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts index 4900f358..9fbdfac6 100644 --- a/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts +++ b/webapp/src/app/components/tcr-session-info/tcr-session-info.component.spec.ts @@ -28,7 +28,6 @@ import {TcrSessionInfoComponent} from "./tcr-session-info.component"; const sample: TcrSessionInfo = { baseDir: "/my/base/dir", - commitOnFail: false, variant: "relaxed", gitAutoPush: false, language: "java", diff --git a/webapp/src/app/interfaces/tcr-session-info.ts b/webapp/src/app/interfaces/tcr-session-info.ts index 64e187ce..a626fb18 100644 --- a/webapp/src/app/interfaces/tcr-session-info.ts +++ b/webapp/src/app/interfaces/tcr-session-info.ts @@ -27,7 +27,6 @@ export interface TcrSessionInfo { toolchain: string; vcsName: string; vcsSession: string; - commitOnFail: boolean; variant: string; gitAutoPush: boolean; messageSuffix: string; diff --git a/webapp/src/app/services/tcr-session-info.service.spec.ts b/webapp/src/app/services/tcr-session-info.service.spec.ts index c8a44cbb..1d4cd490 100644 --- a/webapp/src/app/services/tcr-session-info.service.spec.ts +++ b/webapp/src/app/services/tcr-session-info.service.spec.ts @@ -57,7 +57,6 @@ describe('TcrSessionInfoService', () => { it('should return session info when called', () => { const sample: TcrSessionInfo = { baseDir: "/my/base/dir", - commitOnFail: false, variant: "nice", gitAutoPush: false, language: "java", From 2a06af707750caebb5e78fa0e1662b1c04ec7eb4 Mon Sep 17 00:00:00 2001 From: Philippe Bourgau Date: Fri, 20 Sep 2024 11:29:22 +0200 Subject: [PATCH 33/44] [#674] Rename VCS.Restore to RevertLocal To have clearer names for all VCS - rename interface function - adapt fake --- src/engine/tcr.go | 2 +- src/engine/tcr_test.go | 10 +++++----- src/vcs/fake/vcs_test_fake.go | 26 +++++++++++++------------- src/vcs/git/git_impl.go | 4 ++-- src/vcs/git/git_impl_test.go | 2 +- src/vcs/p4/p4_impl.go | 4 ++-- src/vcs/p4/p4_impl_test.go | 2 +- src/vcs/vcs.go | 2 +- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/engine/tcr.go b/src/engine/tcr.go index 101a2534..fc4ac64e 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -630,7 +630,7 @@ func (tcr *TCREngine) shouldRevertFile(path string) bool { } func (tcr *TCREngine) revertFile(file string) error { - return tcr.vcs.Restore(file) + return tcr.vcs.RevertLocal(file) } // GetSessionInfo provides the information related to the current TCR session. diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index e2f3573f..542d3730 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -216,7 +216,7 @@ func Test_tcr_operation_end_state(t *testing.T) { { "revert with VCS restore failure", func() { - tcr, _ := initTCREngineWithFakes(nil, nil, fake.Commands{fake.RestoreCommand}, nil) + tcr, _ := initTCREngineWithFakes(nil, nil, fake.Commands{fake.RevertLocalCommand}, nil) tcr.revert(*events.ATcrEvent()) }, status.VCSError, @@ -235,7 +235,7 @@ func Test_tcr_operation_end_state(t *testing.T) { func Test_relaxed_source_revert(t *testing.T) { tcr, vcsFake := initTCREngineWithFakes(nil, nil, nil, nil) tcr.revert(*events.ATcrEvent()) - assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) + assert.Equal(t, fake.RevertLocalCommand, vcsFake.GetLastCommand()) } func Test_relaxed_doesnt_revert_test_files(t *testing.T) { @@ -244,7 +244,7 @@ func Test_relaxed_doesnt_revert_test_files(t *testing.T) { vcs.NewFileDiff("fake-test", 1, 1), }) tcr.revert(*events.ATcrEvent()) - assert.NotEqual(t, fake.RestoreCommand, vcsFake.GetLastCommand()) + assert.NotEqual(t, fake.RevertLocalCommand, vcsFake.GetLastCommand()) } func Test_variant_specific_reverts(t *testing.T) { @@ -305,7 +305,7 @@ func Test_variant_specific_reverts(t *testing.T) { nil, nil, nil, tt.fileDiffs) tcr.revert(*events.ATcrEvent()) sniffer.Stop() - assert.Equal(t, fake.RestoreCommand, vcsFake.GetLastCommand()) + assert.Equal(t, fake.RevertLocalCommand, vcsFake.GetLastCommand()) assert.Equal(t, 1, sniffer.GetMatchCount()) }) } @@ -368,7 +368,7 @@ func Test_tcr_cycle_end_state(t *testing.T) { }, { "with test and VCS restore failure", - toolchain.Operations{toolchain.TestOperation}, fake.Commands{fake.RestoreCommand}, + toolchain.Operations{toolchain.TestOperation}, fake.Commands{fake.RevertLocalCommand}, status.VCSError, }, } diff --git a/src/vcs/fake/vcs_test_fake.go b/src/vcs/fake/vcs_test_fake.go index 73c1c284..4a8d1439 100644 --- a/src/vcs/fake/vcs_test_fake.go +++ b/src/vcs/fake/vcs_test_fake.go @@ -51,16 +51,16 @@ func (gc Commands) contains(command Command) bool { // List of supported VCS commands const ( - AddCommand Command = "add" - CommitCommand Command = "commit" - DiffCommand Command = "diff" - LogCommand Command = "log" - PullCommand Command = "pull" - PushCommand Command = "push" - RestoreCommand Command = "restore" - RevertCommand Command = "revert" - StashCommand Command = "stash" - UnStashCommand Command = "unStash" + AddCommand Command = "add" + CommitCommand Command = "commit" + DiffCommand Command = "diff" + LogCommand Command = "log" + PullCommand Command = "pull" + PushCommand Command = "push" + RevertLocalCommand Command = "revertLocal" + RevertCommand Command = "revert" + StashCommand Command = "stash" + UnStashCommand Command = "unStash" ) type ( @@ -125,9 +125,9 @@ func (vf *VCSFake) Commit(_ bool, _ ...string) error { return vf.fakeCommand(CommitCommand) } -// Restore does nothing. Returns an error if in the list of failing commands -func (vf *VCSFake) Restore(_ string) error { - return vf.fakeCommand(RestoreCommand) +// RevertLocal does nothing. Returns an error if in the list of failing commands +func (vf *VCSFake) RevertLocal(_ string) error { + return vf.fakeCommand(RevertLocalCommand) } // Push does nothing. Returns an error if in the list of failing commands diff --git a/src/vcs/git/git_impl.go b/src/vcs/git/git_impl.go index fbff03b1..d7ad2e05 100644 --- a/src/vcs/git/git_impl.go +++ b/src/vcs/git/git_impl.go @@ -251,9 +251,9 @@ func (g *gitImpl) nothingToCommit() bool { return status.IsClean() } -// Restore restores to last commit for the provided path. +// RevertLocal restores to last commit for the provided path. // Current implementation uses a direct call to git -func (g *gitImpl) Restore(path string) error { +func (g *gitImpl) RevertLocal(path string) error { report.PostWarning("Reverting ", path) return g.traceGit("checkout", "HEAD", "--", path) } diff --git a/src/vcs/git/git_impl_test.go b/src/vcs/git/git_impl_test.go index 8449cd95..e9e2bc8a 100644 --- a/src/vcs/git/git_impl_test.go +++ b/src/vcs/git/git_impl_test.go @@ -461,7 +461,7 @@ func Test_git_restore(t *testing.T) { return tt.gitError } - err := g.Restore("some-path") + err := g.RevertLocal("some-path") if tt.expectError { assert.Error(t, err) } else { diff --git a/src/vcs/p4/p4_impl.go b/src/vcs/p4/p4_impl.go index 45bd088f..400b4fb9 100644 --- a/src/vcs/p4/p4_impl.go +++ b/src/vcs/p4/p4_impl.go @@ -162,8 +162,8 @@ func (p *p4Impl) Commit(_ bool, messages ...string) error { return p.submitChangeList(cl) } -// Restore restores to last commit for the provided path. -func (p *p4Impl) Restore(path string) error { +// RevertLocal restores to last commit for the provided path. +func (p *p4Impl) RevertLocal(path string) error { // in order to work, p4 revert requires that the file be reconciled beforehand err := p.reconcile(path) if err != nil { diff --git a/src/vcs/p4/p4_impl_test.go b/src/vcs/p4/p4_impl_test.go index 1a1180fb..d417d394 100644 --- a/src/vcs/p4/p4_impl_test.go +++ b/src/vcs/p4/p4_impl_test.go @@ -551,7 +551,7 @@ func Test_p4_restore(t *testing.T) { return tt.p4Error } - err := p.Restore("some-path") + err := p.RevertLocal("some-path") if tt.expectError { assert.Error(t, err) } else { diff --git a/src/vcs/vcs.go b/src/vcs/vcs.go index f510af9a..3207e803 100644 --- a/src/vcs/vcs.go +++ b/src/vcs/vcs.go @@ -57,7 +57,7 @@ type Interface interface { IsOnRootBranch() bool Add(paths ...string) error Commit(amend bool, messages ...string) error - Restore(path string) error + RevertLocal(path string) error Revert() error Push() error Pull() error From 2fe1061dcb3a0a6c0ed25905e3ccc79b728b47fe Mon Sep 17 00:00:00 2001 From: Philippe Bourgau Date: Fri, 20 Sep 2024 11:32:36 +0200 Subject: [PATCH 34/44] [#674] Rename VCS.Revert to RollbackLastCommit To have clearer names for all VCS - rename interface function - adapt fake --- src/engine/tcr_test.go | 2 +- src/vcs/fake/vcs_test_fake.go | 26 +++++++++++++------------- src/vcs/git/git_impl.go | 4 ++-- src/vcs/git/git_impl_test.go | 2 +- src/vcs/p4/p4_impl.go | 2 +- src/vcs/vcs.go | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 542d3730..fc43ebf5 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -321,7 +321,7 @@ func Test_introspective_variant(t *testing.T) { }) tcr.revert(*events.ATcrEvent()) - assert.Equal(t, []fake.Command{fake.CommitCommand, fake.RevertCommand}, vcsFake.GetLastCommands(2)) + assert.Equal(t, []fake.Command{fake.CommitCommand, fake.RollbackLastCommitCommand}, vcsFake.GetLastCommands(2)) } func Test_tcr_cycle_end_state(t *testing.T) { diff --git a/src/vcs/fake/vcs_test_fake.go b/src/vcs/fake/vcs_test_fake.go index 4a8d1439..e09e393f 100644 --- a/src/vcs/fake/vcs_test_fake.go +++ b/src/vcs/fake/vcs_test_fake.go @@ -51,16 +51,16 @@ func (gc Commands) contains(command Command) bool { // List of supported VCS commands const ( - AddCommand Command = "add" - CommitCommand Command = "commit" - DiffCommand Command = "diff" - LogCommand Command = "log" - PullCommand Command = "pull" - PushCommand Command = "push" - RevertLocalCommand Command = "revertLocal" - RevertCommand Command = "revert" - StashCommand Command = "stash" - UnStashCommand Command = "unStash" + AddCommand Command = "add" + CommitCommand Command = "commit" + DiffCommand Command = "diff" + LogCommand Command = "log" + PullCommand Command = "pull" + PushCommand Command = "push" + RevertLocalCommand Command = "revertLocal" + RollbackLastCommitCommand Command = "rollbackLastCommit" + StashCommand Command = "stash" + UnStashCommand Command = "unStash" ) type ( @@ -172,9 +172,9 @@ func (vf *VCSFake) UnStash(_ bool) error { return vf.fakeCommand(UnStashCommand) } -// Revert does nothing. Returns an error if in the list of failing commands -func (vf *VCSFake) Revert() error { - return vf.fakeCommand(RevertCommand) +// RollbackLastCommit does nothing. Returns an error if in the list of failing commands +func (vf *VCSFake) RollbackLastCommit() error { + return vf.fakeCommand(RollbackLastCommitCommand) } // GetRootDir returns the root directory path diff --git a/src/vcs/git/git_impl.go b/src/vcs/git/git_impl.go index d7ad2e05..76a2a0e0 100644 --- a/src/vcs/git/git_impl.go +++ b/src/vcs/git/git_impl.go @@ -258,9 +258,9 @@ func (g *gitImpl) RevertLocal(path string) error { return g.traceGit("checkout", "HEAD", "--", path) } -// Revert runs a git revert operation. +// RollbackLastCommit runs a git revert operation. // Current implementation uses a direct call to git -func (g *gitImpl) Revert() error { +func (g *gitImpl) RollbackLastCommit() error { report.PostInfo("Reverting changes") return g.traceGit("revert", "--no-gpg-sign", "--no-edit", "HEAD") } diff --git a/src/vcs/git/git_impl_test.go b/src/vcs/git/git_impl_test.go index e9e2bc8a..dec1c884 100644 --- a/src/vcs/git/git_impl_test.go +++ b/src/vcs/git/git_impl_test.go @@ -500,7 +500,7 @@ func Test_git_revert(t *testing.T) { return tt.gitError } - err := g.Revert() + err := g.RollbackLastCommit() if tt.expectError { assert.Error(t, err) } else { diff --git a/src/vcs/p4/p4_impl.go b/src/vcs/p4/p4_impl.go index 400b4fb9..ff492ebc 100644 --- a/src/vcs/p4/p4_impl.go +++ b/src/vcs/p4/p4_impl.go @@ -175,7 +175,7 @@ func (p *p4Impl) RevertLocal(path string) error { // Revert runs a p4 revert operation. // TODO: VCS Revert - p4 revert -func (*p4Impl) Revert() error { +func (*p4Impl) RollbackLastCommit() error { return errors.New("VCS revert operation not yet available for p4") } diff --git a/src/vcs/vcs.go b/src/vcs/vcs.go index 3207e803..bce99e2c 100644 --- a/src/vcs/vcs.go +++ b/src/vcs/vcs.go @@ -58,7 +58,7 @@ type Interface interface { Add(paths ...string) error Commit(amend bool, messages ...string) error RevertLocal(path string) error - Revert() error + RollbackLastCommit() error Push() error Pull() error Stash(message string) error From 195e1551f28c7bb4f8626109b0459e866ed09d23 Mon Sep 17 00:00:00 2001 From: Philippe Bourgau Date: Fri, 20 Sep 2024 11:43:04 +0200 Subject: [PATCH 35/44] [#674] Remove Stash and Unstash from VCS To clean up code now that we don't support commit failures --- src/vcs/fake/vcs_test_fake.go | 12 ---- src/vcs/git/git_impl.go | 19 ------- src/vcs/git/git_impl_test.go | 100 ---------------------------------- src/vcs/p4/p4_impl.go | 13 ----- src/vcs/vcs.go | 2 - 5 files changed, 146 deletions(-) diff --git a/src/vcs/fake/vcs_test_fake.go b/src/vcs/fake/vcs_test_fake.go index e09e393f..91f7f13e 100644 --- a/src/vcs/fake/vcs_test_fake.go +++ b/src/vcs/fake/vcs_test_fake.go @@ -59,8 +59,6 @@ const ( PushCommand Command = "push" RevertLocalCommand Command = "revertLocal" RollbackLastCommitCommand Command = "rollbackLastCommit" - StashCommand Command = "stash" - UnStashCommand Command = "unStash" ) type ( @@ -162,16 +160,6 @@ func (vf *VCSFake) Log(msgFilter func(msg string) bool) (logs vcs.LogItems, err return } -// Stash does nothing. Returns an error if in the list of failing commands -func (vf *VCSFake) Stash(_ string) error { - return vf.fakeCommand(StashCommand) -} - -// UnStash does nothing. Returns an error if in the list of failing commands -func (vf *VCSFake) UnStash(_ bool) error { - return vf.fakeCommand(UnStashCommand) -} - // RollbackLastCommit does nothing. Returns an error if in the list of failing commands func (vf *VCSFake) RollbackLastCommit() error { return vf.fakeCommand(RollbackLastCommitCommand) diff --git a/src/vcs/git/git_impl.go b/src/vcs/git/git_impl.go index 76a2a0e0..a0cf9328 100644 --- a/src/vcs/git/git_impl.go +++ b/src/vcs/git/git_impl.go @@ -292,25 +292,6 @@ func (g *gitImpl) Pull() error { return g.traceGit("pull", "--no-recurse-submodules", g.GetRemoteName(), g.GetWorkingBranch()) } -// Stash creates a git stash. -// Current implementation uses a direct call to git -func (g *gitImpl) Stash(message string) error { - report.PostInfo("Stashing changes") - return g.traceGit("stash", "push", "--quiet", "--include-untracked", "--message", message) -} - -// UnStash applies a git stash. Depending on the keep argument value, either a "stash apply" or a "stash pop" -// command is executed under the hood. -// Current implementation uses a direct call to git -func (g *gitImpl) UnStash(keep bool) error { - report.PostInfo("Applying stashed changes") - stashAction := "pop" - if keep { - stashAction = "apply" - } - return g.traceGit("stash", stashAction, "--quiet") -} - // Diff returns the list of files modified since last commit with diff info for each file // Current implementation uses a direct call to git func (g *gitImpl) Diff() (diffs vcs.FileDiffs, err error) { diff --git a/src/vcs/git/git_impl_test.go b/src/vcs/git/git_impl_test.go index dec1c884..b6224714 100644 --- a/src/vcs/git/git_impl_test.go +++ b/src/vcs/git/git_impl_test.go @@ -511,106 +511,6 @@ func Test_git_revert(t *testing.T) { } } -func Test_git_stash(t *testing.T) { - testFlags := []struct { - desc string - message string - gitError error - expectError bool - expectedArgs []string - }{ - { - "git stash command call succeeds", - "some message", - nil, - false, - []string{"stash", "push", "--quiet", "--include-untracked", "--message", "some message"}, - }, - { - "git stash command call fails", - "some message", - errors.New("git stash push error"), - true, - []string{"stash", "push", "--quiet", "--include-untracked", "--message", "some message"}, - }, - } - for _, tt := range testFlags { - t.Run(tt.desc, func(t *testing.T) { - var actualArgs []string - g, _ := newGitImpl(inMemoryRepoInit, "") - g.traceGitFunction = func(args ...string) (err error) { - actualArgs = args[2:] - return tt.gitError - } - - err := g.Stash(tt.message) - if tt.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - assert.Equal(t, tt.expectedArgs, actualArgs) - }) - } -} - -func Test_git_unstash(t *testing.T) { - testFlags := []struct { - desc string - keep bool - gitError error - expectError bool - expectedArgs []string - }{ - { - "keep stash and git stash command call succeeds", - true, - nil, - false, - []string{"stash", "apply", "--quiet"}, - }, - { - "keep stash and git stash command call fails", - true, - errors.New("git stash apply error"), - true, - []string{"stash", "apply", "--quiet"}, - }, - { - "remove stash and git stash command call succeeds", - false, - nil, - false, - []string{"stash", "pop", "--quiet"}, - }, - { - "remove stash and git stash command call fails", - false, - errors.New("git stash pop error"), - true, - []string{"stash", "pop", "--quiet"}, - }, - } - for _, tt := range testFlags { - t.Run(tt.desc, func(t *testing.T) { - var actualArgs []string - g, _ := newGitImpl(inMemoryRepoInit, "") - g.traceGitFunction = func(args ...string) (err error) { - actualArgs = args[2:] - return tt.gitError - } - - err := g.UnStash(tt.keep) - if tt.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - assert.Equal(t, tt.expectedArgs, actualArgs) - }) - } -} - func Test_git_log(t *testing.T) { // Note: this test may break if for any reason the TCR repository initial commit is altered tcrInitialCommit := vcs.LogItem{ diff --git a/src/vcs/p4/p4_impl.go b/src/vcs/p4/p4_impl.go index ff492ebc..068a0913 100644 --- a/src/vcs/p4/p4_impl.go +++ b/src/vcs/p4/p4_impl.go @@ -195,19 +195,6 @@ func (p *p4Impl) Pull() error { return p.traceP4("sync", path) } -// Stash creates a p4 stash. -// TODO: VCS Stash - p4 ??? -func (*p4Impl) Stash(_ string) error { - return errors.New("VCS stash operation not yet available for p4") -} - -// UnStash applies a p4 stash. Depending on the keep argument value, either a "stash apply" or a "stash pop" -// command is executed under the hood. -// TODO: VCS UnStash - p4 ??? -func (*p4Impl) UnStash(_ bool) error { - return errors.New("VCS unstash operation not yet available for p4") -} - // Diff returns the list of files modified since last commit with diff info for each file func (p *p4Impl) Diff() (diffs vcs.FileDiffs, err error) { var p4Output []byte diff --git a/src/vcs/vcs.go b/src/vcs/vcs.go index bce99e2c..65cf00dd 100644 --- a/src/vcs/vcs.go +++ b/src/vcs/vcs.go @@ -61,8 +61,6 @@ type Interface interface { RollbackLastCommit() error Push() error Pull() error - Stash(message string) error - UnStash(keep bool) error Diff() (diffs FileDiffs, err error) Log(msgFilter func(msg string) bool) (logs LogItems, err error) EnableAutoPush(flag bool) From 426a2868845ec1542bf6576d8ea8262b4b706d31 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Fri, 20 Sep 2024 12:01:30 +0200 Subject: [PATCH 36/44] [#674] Implement the introspective variant in tcr.revert() - add tcr.introspectiveRevert() - inject call to introspectiveRevert() in tcr.revert() - tune and pass test case on revert for introspective variant --- src/engine/tcr.go | 26 +++++++++++++++++++++++++- src/engine/tcr_test.go | 8 ++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/engine/tcr.go b/src/engine/tcr.go index fc4ac64e..8feb6094 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -595,7 +595,11 @@ func (tcr *TCREngine) commit(event events.TCREvent) { tcr.handleError(tcr.vcsPushAuto(), false, status.VCSError) } -func (tcr *TCREngine) revert(_ events.TCREvent) { +func (tcr *TCREngine) revert(e events.TCREvent) { + if *tcr.variant == variant.Introspective { + _ = tcr.introspectiveRevert(e) + return + } diffs, err := tcr.vcs.Diff() tcr.handleError(err, false, status.VCSError) if err != nil { @@ -618,6 +622,26 @@ func (tcr *TCREngine) revert(_ events.TCREvent) { } } +func (tcr *TCREngine) introspectiveRevert(event events.TCREvent) (err error) { + // Commit changes with failure message into VCS index + err = tcr.vcs.Add() + if err != nil { + return err + } + err = tcr.vcs.Commit(false, tcr.wrapCommitMessages(commitMessageFail, &event)...) + if err != nil { + return err + } + // Rollback changes (both in VCS index and working tree) + err = tcr.vcs.RollbackLastCommit() + if err != nil { + return err + } + // Amend commit message on revert operation in VCS index + err = tcr.vcs.Commit(false, tcr.wrapCommitMessages(commitMessageRevert, nil)...) + return err +} + func (tcr *TCREngine) noFilesRevertedMessage() string { if *tcr.variant == variant.Relaxed { return "No file reverted (only test files were updated since last commit)" diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index fc43ebf5..401ef7d5 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -312,7 +312,6 @@ func Test_variant_specific_reverts(t *testing.T) { } func Test_introspective_variant(t *testing.T) { - t.Skip("work in progress") tcr, vcsFake := initTCREngineWithFakesWithFileDiffs( params.AParamSet(params.WithVariant(variant.Introspective.Name())), nil, nil, nil, vcs.FileDiffs{ @@ -321,7 +320,12 @@ func Test_introspective_variant(t *testing.T) { }) tcr.revert(*events.ATcrEvent()) - assert.Equal(t, []fake.Command{fake.CommitCommand, fake.RollbackLastCommitCommand}, vcsFake.GetLastCommands(2)) + assert.Equal(t, []fake.Command{ + fake.AddCommand, + fake.CommitCommand, + fake.RollbackLastCommitCommand, + fake.CommitCommand, + }, vcsFake.GetLastCommands(4)) } func Test_tcr_cycle_end_state(t *testing.T) { From b5136309f668066de2b98db4efcafa94ab7a479c Mon Sep 17 00:00:00 2001 From: Ahmad ATWI Date: Mon, 23 Sep 2024 11:07:11 +0200 Subject: [PATCH 37/44] [#674] Implement and test the introspective variant - Clean the tcr.revert function - Added an array in vcs fake to track the commit subjects - Add tests to verify the output of messages --- src/engine/tcr.go | 9 +++-- src/engine/tcr_test.go | 66 +++++++++++++++++++++++++++++++++-- src/vcs/fake/vcs_test_fake.go | 16 ++++++--- 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/src/engine/tcr.go b/src/engine/tcr.go index 8feb6094..8faeef74 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -596,10 +596,15 @@ func (tcr *TCREngine) commit(event events.TCREvent) { } func (tcr *TCREngine) revert(e events.TCREvent) { - if *tcr.variant == variant.Introspective { + switch *tcr.variant { + case variant.Introspective: _ = tcr.introspectiveRevert(e) - return + default: + tcr.simpleRevert() } +} + +func (tcr *TCREngine) simpleRevert() { diffs, err := tcr.vcs.Diff() tcr.handleError(err, false, status.VCSError) if err != nil { diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 401ef7d5..64cf9b5a 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -214,7 +214,7 @@ func Test_tcr_operation_end_state(t *testing.T) { status.VCSError, }, { - "revert with VCS restore failure", + "revert with VCS revert local failure", func() { tcr, _ := initTCREngineWithFakes(nil, nil, fake.Commands{fake.RevertLocalCommand}, nil) tcr.revert(*events.ATcrEvent()) @@ -371,7 +371,7 @@ func Test_tcr_cycle_end_state(t *testing.T) { status.VCSError, }, { - "with test and VCS restore failure", + "with test and VCS revert local failure", toolchain.Operations{toolchain.TestOperation}, fake.Commands{fake.RevertLocalCommand}, status.VCSError, }, @@ -527,6 +527,68 @@ func Test_set_variant(t *testing.T) { } } +func Test_variant_commit_subjects(t *testing.T) { + var tcr TCRInterface + testFlags := []struct { + description string + variant variant.Variant + toolchainFailures toolchain.Operations + expected []string + }{ + { + "Introspective with tests passing.", + variant.Introspective, + toolchain.Operations{}, + []string{"✅ TCR - tests passing"}, + }, + { + "Introspective with tests failing.", + variant.Introspective, + toolchain.Operations{toolchain.TestOperation}, + []string{ + "❌ TCR - tests failing", + "⏪ TCR - revert changes", + }, + }, + { + "Relaxed with tests passing.", + variant.Relaxed, + toolchain.Operations{}, + []string{"✅ TCR - tests passing"}, + }, + { + "Relaxed with tests failing.", + variant.Relaxed, + toolchain.Operations{toolchain.TestOperation}, + []string{}, + }, + { + "BTCR with tests passing.", + variant.BTCR, + toolchain.Operations{}, + []string{"✅ TCR - tests passing"}, + }, + { + "BTCR with tests failing.", + variant.BTCR, + toolchain.Operations{toolchain.TestOperation}, + []string{}, + }, + } + + for _, tt := range testFlags { + t.Run(tt.description, func(t *testing.T) { + var vcsFake *fake.VCSFake + tcr, vcsFake = initTCREngineWithFakes(nil, tt.toolchainFailures, nil, nil) + tcr.SetVariant(tt.variant.Name()) + tcr.RunTCRCycle() + messages := vcsFake.GetLastCommitSubjects() + assert.Equal(t, tt.expected, messages) + }) + } + +} + func Test_vcs_pull_calls_vcs_command(t *testing.T) { tcr, vcsFake := initTCREngineWithFakes(nil, nil, nil, nil) tcr.VCSPull() diff --git a/src/vcs/fake/vcs_test_fake.go b/src/vcs/fake/vcs_test_fake.go index 91f7f13e..09109169 100644 --- a/src/vcs/fake/vcs_test_fake.go +++ b/src/vcs/fake/vcs_test_fake.go @@ -73,9 +73,10 @@ type ( // VCSFake provides a fake implementation of the VCS interface VCSFake struct { - settings Settings - pushEnabled bool - lastCommands []Command + settings Settings + pushEnabled bool + lastCommands []Command + lastCommitSubjects []string } ) @@ -90,7 +91,7 @@ func (vf *VCSFake) fakeCommand(cmd Command) (err error) { // NewVCSFake initializes a fake VCS implementation which does nothing // apart from emulating errors on VCS operations func NewVCSFake(settings Settings) *VCSFake { - return &VCSFake{settings: settings, lastCommands: make([]Command, 0)} + return &VCSFake{settings: settings, lastCommitSubjects: make([]string, 0), lastCommands: make([]Command, 0)} } // Name returns VCS name @@ -119,7 +120,8 @@ func (vf *VCSFake) Add(_ ...string) error { } // Commit does nothing. Returns an error if in the list of failing commands -func (vf *VCSFake) Commit(_ bool, _ ...string) error { +func (vf *VCSFake) Commit(_ bool, messages ...string) error { + vf.lastCommitSubjects = append(vf.lastCommitSubjects, messages[0]) return vf.fakeCommand(CommitCommand) } @@ -204,3 +206,7 @@ func (vf *VCSFake) IsRemoteEnabled() bool { func (vf *VCSFake) CheckRemoteAccess() bool { return vf.settings.RemoteAccessWorking } + +func (vf *VCSFake) GetLastCommitSubjects() []string { + return vf.lastCommitSubjects +} From 8e2d0d2c1da122aaeaf7ef7af7ef4080ad3240e5 Mon Sep 17 00:00:00 2001 From: Philippe Bourgau Date: Mon, 23 Sep 2024 11:30:23 +0200 Subject: [PATCH 38/44] [#674] Remove the amend parameter from the VCS interface Because we don't need it anymore without the 'commit failure' option - remove parameter from interface - adapt implementations - simplify tests - adapt calls --- src/engine/tcr.go | 6 +++--- src/vcs/fake/vcs_test_fake.go | 2 +- src/vcs/git/git_impl.go | 5 +---- src/vcs/git/git_impl_test.go | 18 +++++------------- src/vcs/p4/p4_impl.go | 2 +- src/vcs/p4/p4_impl_test.go | 20 ++++++-------------- src/vcs/vcs.go | 2 +- 7 files changed, 18 insertions(+), 37 deletions(-) diff --git a/src/engine/tcr.go b/src/engine/tcr.go index 8faeef74..6aebebe7 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -587,7 +587,7 @@ func (tcr *TCREngine) commit(event events.TCREvent) { if err != nil { return } - err = tcr.vcs.Commit(false, tcr.wrapCommitMessages(commitMessageOk, &event)...) + err = tcr.vcs.Commit(tcr.wrapCommitMessages(commitMessageOk, &event)...) tcr.handleError(err, false, status.VCSError) if err != nil { return @@ -633,7 +633,7 @@ func (tcr *TCREngine) introspectiveRevert(event events.TCREvent) (err error) { if err != nil { return err } - err = tcr.vcs.Commit(false, tcr.wrapCommitMessages(commitMessageFail, &event)...) + err = tcr.vcs.Commit(tcr.wrapCommitMessages(commitMessageFail, &event)...) if err != nil { return err } @@ -643,7 +643,7 @@ func (tcr *TCREngine) introspectiveRevert(event events.TCREvent) (err error) { return err } // Amend commit message on revert operation in VCS index - err = tcr.vcs.Commit(false, tcr.wrapCommitMessages(commitMessageRevert, nil)...) + err = tcr.vcs.Commit(tcr.wrapCommitMessages(commitMessageRevert, nil)...) return err } diff --git a/src/vcs/fake/vcs_test_fake.go b/src/vcs/fake/vcs_test_fake.go index 09109169..277a88e0 100644 --- a/src/vcs/fake/vcs_test_fake.go +++ b/src/vcs/fake/vcs_test_fake.go @@ -120,7 +120,7 @@ func (vf *VCSFake) Add(_ ...string) error { } // Commit does nothing. Returns an error if in the list of failing commands -func (vf *VCSFake) Commit(_ bool, messages ...string) error { +func (vf *VCSFake) Commit(messages ...string) error { vf.lastCommitSubjects = append(vf.lastCommitSubjects, messages[0]) return vf.fakeCommand(CommitCommand) } diff --git a/src/vcs/git/git_impl.go b/src/vcs/git/git_impl.go index a0cf9328..8f74aab6 100644 --- a/src/vcs/git/git_impl.go +++ b/src/vcs/git/git_impl.go @@ -225,11 +225,8 @@ func (g *gitImpl) Add(paths ...string) error { // Commit commits changes to git index. // Current implementation uses a direct call to git -func (g *gitImpl) Commit(amend bool, messages ...string) error { +func (g *gitImpl) Commit(messages ...string) error { gitArgs := []string{"commit", "--no-gpg-sign"} - if amend { - gitArgs = append(gitArgs, "--amend") - } for _, message := range messages { gitArgs = append(gitArgs, "-m", message) } diff --git a/src/vcs/git/git_impl_test.go b/src/vcs/git/git_impl_test.go index b6224714..5c0d99ec 100644 --- a/src/vcs/git/git_impl_test.go +++ b/src/vcs/git/git_impl_test.go @@ -377,46 +377,38 @@ func Test_git_commit(t *testing.T) { testFlags := []struct { desc string messages []string - amend bool gitError error expectError bool expectedArgs []string }{ { "git commit command call succeeds", - []string{"some message"}, false, + []string{"some message"}, nil, false, []string{"commit", "--no-gpg-sign", "-m", "some message"}, }, { "git commit command call fails", - []string{"some message"}, false, + []string{"some message"}, errors.New("git commit error"), false, // We currently ignore git commit errors to handle the case when there's nothing to commit []string{"commit", "--no-gpg-sign", "-m", "some message"}, }, { "with multiple messages", - []string{"main message", "additional message"}, false, + []string{"main message", "additional message"}, nil, false, []string{"commit", "--no-gpg-sign", "-m", "main message", "-m", "additional message"}, }, { "with multi-line messages", - []string{"main message", "- line 1\n- line 2"}, false, + []string{"main message", "- line 1\n- line 2"}, nil, false, []string{"commit", "--no-gpg-sign", "-m", "main message", "-m", "- line 1\n- line 2"}, }, - { - "with amend option", - []string{"some message"}, true, - nil, - false, - []string{"commit", "--no-gpg-sign", "--amend", "-m", "some message"}, - }, } for _, tt := range testFlags { t.Run(tt.desc, func(t *testing.T) { @@ -426,7 +418,7 @@ func Test_git_commit(t *testing.T) { actualArgs = args[2:] return tt.gitError } - err := g.Commit(tt.amend, tt.messages...) + err := g.Commit(tt.messages...) if tt.expectError { assert.Error(t, err) } else { diff --git a/src/vcs/p4/p4_impl.go b/src/vcs/p4/p4_impl.go index 068a0913..d21a6b39 100644 --- a/src/vcs/p4/p4_impl.go +++ b/src/vcs/p4/p4_impl.go @@ -153,7 +153,7 @@ type changeList struct { // Commit commits changes to p4 index. // With current implementation, "amend" parameter is ignored. -func (p *p4Impl) Commit(_ bool, messages ...string) error { +func (p *p4Impl) Commit(messages ...string) error { cl, err := p.createChangeList(messages...) if err != nil { report.PostError(err) diff --git a/src/vcs/p4/p4_impl_test.go b/src/vcs/p4/p4_impl_test.go index d417d394..e30605db 100644 --- a/src/vcs/p4/p4_impl_test.go +++ b/src/vcs/p4/p4_impl_test.go @@ -393,7 +393,6 @@ func Test_p4_commit(t *testing.T) { testFlags := []struct { desc string messages []string - amend bool p4ChangeError error p4ChangeOutput string p4SubmitError error @@ -402,42 +401,35 @@ func Test_p4_commit(t *testing.T) { }{ { "p4 change and p4 submit command calls succeed", - []string{"some message"}, false, + []string{"some message"}, nil, "change 1234567 created ...", nil, []string{"submit", "-c", "1234567"}, false, }, { "p4 change command call fails", - []string{"some message"}, false, + []string{"some message"}, errors.New("p4 change error"), "", nil, nil, true, }, { "p4 submit command call fails", - []string{"some message"}, false, + []string{"some message"}, nil, "change 1234567 created ...", errors.New("p4 submit error"), []string{"submit", "-c", "1234567"}, true, }, { "with multiple messages", - []string{"main message", "additional message"}, false, + []string{"main message", "additional message"}, nil, "change 1234567 created ...", nil, []string{"submit", "-c", "1234567"}, false, }, { "with multi-line messages", - []string{"main message", "- line 1\n- line 2"}, false, - nil, "change 1234567 created ...", - nil, []string{"submit", "-c", "1234567"}, - false, - }, - { - "with amend option", - []string{"some message"}, true, + []string{"main message", "- line 1\n- line 2"}, nil, "change 1234567 created ...", nil, []string{"submit", "-c", "1234567"}, false, @@ -457,7 +449,7 @@ func Test_p4_commit(t *testing.T) { return tt.p4SubmitError } - err := p.Commit(tt.amend, tt.messages...) + err := p.Commit(tt.messages...) if tt.expectError { assert.Error(t, err) } else { diff --git a/src/vcs/vcs.go b/src/vcs/vcs.go index 65cf00dd..20e39fa5 100644 --- a/src/vcs/vcs.go +++ b/src/vcs/vcs.go @@ -56,7 +56,7 @@ type Interface interface { GetWorkingBranch() string IsOnRootBranch() bool Add(paths ...string) error - Commit(amend bool, messages ...string) error + Commit(messages ...string) error RevertLocal(path string) error RollbackLastCommit() error Push() error From c7ac587ebe53ebc877e6aca1d42cae27e4239f75 Mon Sep 17 00:00:00 2001 From: Philippe Bourgau Date: Mon, 23 Sep 2024 11:55:17 +0200 Subject: [PATCH 39/44] [#674] Handle VCS errors in introspectiveRevert - refactor simple and introspective Reverts to simplify error handling - add a test to make sure we deal with VCS errors in introspectiveRevert --- src/engine/tcr.go | 23 ++++++++++++----------- src/engine/tcr_test.go | 9 +++++++++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/engine/tcr.go b/src/engine/tcr.go index 6aebebe7..57942bad 100644 --- a/src/engine/tcr.go +++ b/src/engine/tcr.go @@ -596,28 +596,31 @@ func (tcr *TCREngine) commit(event events.TCREvent) { } func (tcr *TCREngine) revert(e events.TCREvent) { + var err error + switch *tcr.variant { case variant.Introspective: - _ = tcr.introspectiveRevert(e) + err = tcr.introspectiveRevert(e) default: - tcr.simpleRevert() + err = tcr.simpleRevert() } + + tcr.handleError(err, false, status.VCSError) } -func (tcr *TCREngine) simpleRevert() { +func (tcr *TCREngine) simpleRevert() error { diffs, err := tcr.vcs.Diff() - tcr.handleError(err, false, status.VCSError) if err != nil { - return + return err } var reverted int for _, diff := range diffs { if tcr.shouldRevertFile(diff.Path) { err := tcr.revertFile(diff.Path) - tcr.handleError(err, false, status.VCSError) - if err == nil { - reverted++ + if err != nil { + return err } + reverted++ } } if reverted > 0 { @@ -625,10 +628,10 @@ func (tcr *TCREngine) simpleRevert() { } else { report.PostInfo(tcr.noFilesRevertedMessage()) } + return nil } func (tcr *TCREngine) introspectiveRevert(event events.TCREvent) (err error) { - // Commit changes with failure message into VCS index err = tcr.vcs.Add() if err != nil { return err @@ -637,12 +640,10 @@ func (tcr *TCREngine) introspectiveRevert(event events.TCREvent) (err error) { if err != nil { return err } - // Rollback changes (both in VCS index and working tree) err = tcr.vcs.RollbackLastCommit() if err != nil { return err } - // Amend commit message on revert operation in VCS index err = tcr.vcs.Commit(tcr.wrapCommitMessages(commitMessageRevert, nil)...) return err } diff --git a/src/engine/tcr_test.go b/src/engine/tcr_test.go index 64cf9b5a..3b543eee 100644 --- a/src/engine/tcr_test.go +++ b/src/engine/tcr_test.go @@ -221,6 +221,15 @@ func Test_tcr_operation_end_state(t *testing.T) { }, status.VCSError, }, + { + "introspective revert with VCS rollback last commit failure", + func() { + tcr, _ := initTCREngineWithFakes(nil, nil, fake.Commands{fake.RollbackLastCommitCommand}, nil) + tcr.SetVariant(variant.Introspective.Name()) + tcr.revert(*events.ATcrEvent()) + }, + status.VCSError, + }, } for _, tt := range testFlags { From 64bf95e9474251178f498a44a1add1b27b7c16fe Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Fri, 27 Sep 2024 10:33:29 +0200 Subject: [PATCH 40/44] [#674] Tune git revert command arguments to prevent auto-commit So that we can use our own commit message when reverting - add option "--no-commit" to git revert command - tune git package tests accordingly --- src/vcs/git/git_impl.go | 2 +- src/vcs/git/git_impl_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vcs/git/git_impl.go b/src/vcs/git/git_impl.go index 8f74aab6..b912a4c2 100644 --- a/src/vcs/git/git_impl.go +++ b/src/vcs/git/git_impl.go @@ -259,7 +259,7 @@ func (g *gitImpl) RevertLocal(path string) error { // Current implementation uses a direct call to git func (g *gitImpl) RollbackLastCommit() error { report.PostInfo("Reverting changes") - return g.traceGit("revert", "--no-gpg-sign", "--no-edit", "HEAD") + return g.traceGit("revert", "--no-gpg-sign", "--no-edit", "--no-commit", "HEAD") } // Push runs a git push operation. diff --git a/src/vcs/git/git_impl_test.go b/src/vcs/git/git_impl_test.go index 5c0d99ec..ad0c05f8 100644 --- a/src/vcs/git/git_impl_test.go +++ b/src/vcs/git/git_impl_test.go @@ -474,13 +474,13 @@ func Test_git_revert(t *testing.T) { "git revert command call succeeds", nil, false, - []string{"revert", "--no-gpg-sign", "--no-edit", "HEAD"}, + []string{"revert", "--no-gpg-sign", "--no-edit", "--no-commit", "HEAD"}, }, { "git revert command call fails", errors.New("git revert error"), true, - []string{"revert", "--no-gpg-sign", "--no-edit", "HEAD"}, + []string{"revert", "--no-gpg-sign", "--no-edit", "--no-commit", "HEAD"}, }, } for _, tt := range testFlags { From 6ed2126f467767d343049dd9c65d8e7641ff28df Mon Sep 17 00:00:00 2001 From: Ahmad ATWI Date: Fri, 27 Sep 2024 11:39:13 +0200 Subject: [PATCH 41/44] [#674] Implement the Variant Checker - Implement the function variant.checkVariant - Rename checkWorkflowConfiguration to checkVariantConfiguration - Add tests to verify the function variant.checkVariant - Allowed variant names to be case-insensitive --- .../{check_workflow.go => check_variant.go} | 28 ++++-- src/checker/check_variant_test.go | 89 +++++++++++++++++++ src/checker/check_workflow_test.go | 36 -------- src/checker/runner.go | 2 +- src/variant/variant.go | 7 +- src/variant/variant_test.go | 2 + 6 files changed, 116 insertions(+), 48 deletions(-) rename src/checker/{check_workflow.go => check_variant.go} (57%) create mode 100644 src/checker/check_variant_test.go delete mode 100644 src/checker/check_workflow_test.go diff --git a/src/checker/check_workflow.go b/src/checker/check_variant.go similarity index 57% rename from src/checker/check_workflow.go rename to src/checker/check_variant.go index 325c6e89..3446ca06 100644 --- a/src/checker/check_workflow.go +++ b/src/checker/check_variant.go @@ -25,26 +25,36 @@ package checker import ( "github.com/murex/tcr/checker/model" "github.com/murex/tcr/params" + "github.com/murex/tcr/variant" + "strings" ) -var checkWorkflowRunners []checkPointRunner +var checkVariantRunners []checkPointRunner func init() { - checkWorkflowRunners = []checkPointRunner{ - checkVariant, + checkVariantRunners = []checkPointRunner{ + checkVariantSelection, } } -func checkWorkflowConfiguration(p params.Params) (cg *model.CheckGroup) { - cg = model.NewCheckGroup("TCR workflow configuration") - for _, runner := range checkWorkflowRunners { +func checkVariantConfiguration(p params.Params) (cg *model.CheckGroup) { + cg = model.NewCheckGroup("TCR variant configuration") + for _, runner := range checkVariantRunners { cg.Add(runner(p)...) } return cg } -func checkVariant(_ params.Params) (cp []model.CheckPoint) { - cp = append(cp, model.WarningCheckPoint( - "variant checker TODO")) +func checkVariantSelection(p params.Params) (cp []model.CheckPoint) { + switch variantName := strings.ToLower(p.Variant); variantName { + case variant.Relaxed.Name(), variant.BTCR.Name(), variant.Introspective.Name(): + cp = append(cp, model.OkCheckPoint("selected variant is ", variantName)) + case "original": + cp = append(cp, model.ErrorCheckPoint("original variant is not yet supported")) + case "": + cp = append(cp, model.ErrorCheckPoint("no variant is selected")) + default: + cp = append(cp, model.ErrorCheckPoint("selected variant is not supported: \"", variantName, "\"")) + } return cp } diff --git a/src/checker/check_variant_test.go b/src/checker/check_variant_test.go new file mode 100644 index 00000000..adb97c64 --- /dev/null +++ b/src/checker/check_variant_test.go @@ -0,0 +1,89 @@ +/* +Copyright (c) 2023 Murex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package checker + +import ( + "github.com/murex/tcr/checker/model" + "github.com/murex/tcr/params" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_check_variant_configuration(t *testing.T) { + assertCheckGroupRunner(t, + checkVariantConfiguration, + &checkVariantRunners, + *params.AParamSet(), + "TCR variant configuration") +} + +func Test_check_variant_selection(t *testing.T) { + tests := []struct { + desc string + variantName string + expected []model.CheckPoint + }{ + { + "empty", "", + []model.CheckPoint{ + model.ErrorCheckPoint("no variant is selected"), + }, + }, + { + "unknown", "unknown-variant", + []model.CheckPoint{ + model.ErrorCheckPoint("selected variant is not supported: \"unknown-variant\""), + }, + }, + { + "Original", "original", + []model.CheckPoint{ + model.ErrorCheckPoint("original variant is not yet supported"), + }, + }, + { + "Introspective", "introspective", + []model.CheckPoint{ + model.OkCheckPoint("selected variant is introspective"), + }, + }, + { + "BTCR", "btcr", + []model.CheckPoint{ + model.OkCheckPoint("selected variant is btcr"), + }, + }, + { + "Relaxed", "relaxed", + []model.CheckPoint{ + model.OkCheckPoint("selected variant is relaxed"), + }, + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + p := *params.AParamSet(params.WithVariant(test.variantName)) + assert.Equal(t, test.expected, checkVariantSelection(p)) + }) + } +} diff --git a/src/checker/check_workflow_test.go b/src/checker/check_workflow_test.go deleted file mode 100644 index 5d7d275f..00000000 --- a/src/checker/check_workflow_test.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright (c) 2023 Murex - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -package checker - -import ( - "github.com/murex/tcr/params" - "testing" -) - -func Test_check_workflow_configuration(t *testing.T) { - assertCheckGroupRunner(t, - checkWorkflowConfiguration, - &checkWorkflowRunners, - *params.AParamSet(), - "TCR workflow configuration") -} diff --git a/src/checker/runner.go b/src/checker/runner.go index 8a8002f2..05c87446 100644 --- a/src/checker/runner.go +++ b/src/checker/runner.go @@ -61,7 +61,7 @@ var checkGroupRunners = []checkGroupRunner{ checkVCSConfiguration, checkGitEnvironment, checkP4Environment, - checkWorkflowConfiguration, + checkVariantConfiguration, checkMobConfiguration, } diff --git a/src/variant/variant.go b/src/variant/variant.go index 8fd189df..c086989c 100644 --- a/src/variant/variant.go +++ b/src/variant/variant.go @@ -22,7 +22,10 @@ SOFTWARE. package variant -import "fmt" +import ( + "fmt" + "strings" +) // UnsupportedVariantError is returned when the provided Variant name is not supported. type UnsupportedVariantError struct { @@ -53,7 +56,7 @@ var recognized = []Variant{Relaxed, BTCR, Introspective} // valid variant name. func Select(name string) (*Variant, error) { for _, variant := range recognized { - if name == variant.Name() { + if strings.ToLower(name) == strings.ToLower(variant.Name()) { return &variant, nil } } diff --git a/src/variant/variant_test.go b/src/variant/variant_test.go index 1cbc29f8..40d6883f 100644 --- a/src/variant/variant_test.go +++ b/src/variant/variant_test.go @@ -53,7 +53,9 @@ func Test_select_variant(t *testing.T) { expectedError error }{ {"relaxed", &relaxed, nil}, + {"Relaxed", &relaxed, nil}, {"btcr", &btcr, nil}, + {"BTCR", &btcr, nil}, {"introspective", &introspective, nil}, {"unknown", nil, &UnsupportedVariantError{"unknown"}}, {"", nil, &UnsupportedVariantError{""}}, From 04af96c93e919415b52476c760f3cd56ba9d4ea5 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Mon, 30 Sep 2024 10:21:52 +0200 Subject: [PATCH 42/44] [#674] Add short section about variants in README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 8989cd52..4d5cedef 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,16 @@ provided as built-in. | scala | sbt | sbt | | typescript | yarn | yarn bazel make | +### TCR Variants + +TCR tool can run several variants of the TCR workflow, inspired by [this blog post](https://medium.com/@tdeniffel/tcr-variants-test-commit-revert-bf6bd84b17d3) +by Thomas Deniffel. + +Some are great as pedagogic tools, some are better for day-to-day use. + +The default variant is "The Relaxed". You can refer to [this page](./variants-doc/tcr_variants.md) +for further details on available variants. + ### Base directory In order to know which files and directories to watch, TCR needs to know on which part of the filesystem it should work. From 89fe9b228cc3fd447e7364e8a17c98cbe0614b40 Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Tue, 1 Oct 2024 11:33:38 +0200 Subject: [PATCH 43/44] [#674] Prepare for p4 implementation of RollbackLastCommit() - renamed test cases from restore to revert_local - renamed git test case from revert to rollback_last_commit - added p4 rollback_last_commit test case (wip) --- src/vcs/git/git_impl_test.go | 4 ++-- src/vcs/p4/p4_impl.go | 3 +-- src/vcs/p4/p4_impl_test.go | 44 +++++++++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/vcs/git/git_impl_test.go b/src/vcs/git/git_impl_test.go index ad0c05f8..cac9c451 100644 --- a/src/vcs/git/git_impl_test.go +++ b/src/vcs/git/git_impl_test.go @@ -429,7 +429,7 @@ func Test_git_commit(t *testing.T) { } } -func Test_git_restore(t *testing.T) { +func Test_git_revert_local(t *testing.T) { testFlags := []struct { desc string gitError error @@ -463,7 +463,7 @@ func Test_git_restore(t *testing.T) { } } -func Test_git_revert(t *testing.T) { +func Test_git_rollback_last_commit(t *testing.T) { testFlags := []struct { desc string gitError error diff --git a/src/vcs/p4/p4_impl.go b/src/vcs/p4/p4_impl.go index d21a6b39..06f70674 100644 --- a/src/vcs/p4/p4_impl.go +++ b/src/vcs/p4/p4_impl.go @@ -173,8 +173,7 @@ func (p *p4Impl) RevertLocal(path string) error { return p.traceP4("revert", path) } -// Revert runs a p4 revert operation. -// TODO: VCS Revert - p4 revert +// RollbackLastCommit runs a p4 revert operation. func (*p4Impl) RollbackLastCommit() error { return errors.New("VCS revert operation not yet available for p4") } diff --git a/src/vcs/p4/p4_impl_test.go b/src/vcs/p4/p4_impl_test.go index e30605db..2fe532e5 100644 --- a/src/vcs/p4/p4_impl_test.go +++ b/src/vcs/p4/p4_impl_test.go @@ -508,7 +508,7 @@ func Test_p4_submit(t *testing.T) { } } -func Test_p4_restore(t *testing.T) { +func Test_p4_revert_local(t *testing.T) { testFlags := []struct { desc string p4Error error @@ -556,6 +556,48 @@ func Test_p4_restore(t *testing.T) { } } +func Test_p4_rollback_last_commit(t *testing.T) { + t.Skip("Work in progress") + + testFlags := []struct { + desc string + p4Error error + expectError bool + expectedArgs []string + }{ + { + "p4 undo command call succeeds", + nil, + false, + []string{"undo", "--no-gpg-sign", "--no-edit", "--no-commit", "HEAD"}, + }, + { + "git revert command call fails", + errors.New("git revert error"), + true, + []string{"revert", "--no-gpg-sign", "--no-edit", "--no-commit", "HEAD"}, + }, + } + for _, tt := range testFlags { + t.Run(tt.desc, func(t *testing.T) { + var actualArgs []string + p, _ := newP4Impl(inMemoryDepotInit, "", true) + p.traceP4Function = func(args ...string) (err error) { + actualArgs = args[2:] + return tt.p4Error + } + + err := p.RollbackLastCommit() + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expectedArgs, actualArgs) + }) + } +} + func Test_convert_to_p4_client_path(t *testing.T) { testFlags := []struct { desc string From 1b6ce20460e25fb4118aaaecc58c5df416c0d80e Mon Sep 17 00:00:00 2001 From: Damien Menanteau Date: Tue, 1 Oct 2024 11:34:20 +0200 Subject: [PATCH 44/44] [#674] Add "invariant" to variant CLI parameter short description --- src/config/param_variant.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/param_variant.go b/src/config/param_variant.go index 5e45acb3..e82f9a9f 100644 --- a/src/config/param_variant.go +++ b/src/config/param_variant.go @@ -39,7 +39,7 @@ func AddVariantParam(cmd *cobra.Command) *StringParam { cobraSettings: cobraSettings{ name: "variant", shorthand: "r", - usage: "indicate the variant to be used by TCR: relaxed (default) or btcr", + usage: "indicate the variant to be used by TCR: relaxed (default), btcr, or introspective", persistent: true, }, },