diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/AzPredictorTests.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/AzPredictorTests.cs index df52a44ba337..d1f9f3f4aa55 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/AzPredictorTests.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/AzPredictorTests.cs @@ -37,12 +37,12 @@ public sealed class AzPredictorTests /// public AzPredictorTests(ModelFixture modelFixture) { - this._fixture = modelFixture; + _fixture = modelFixture; var startHistory = $"{AzPredictorConstants.CommandPlaceholder}{AzPredictorConstants.CommandConcatenator}{AzPredictorConstants.CommandPlaceholder}"; - this._service = new MockAzPredictorService(startHistory, this._fixture.PredictionCollection[startHistory], this._fixture.CommandCollection); - this._telemetryClient = new MockAzPredictorTelemetryClient(); - this._azPredictor = new AzPredictor(this._service, this._telemetryClient, new Settings() + _service = new MockAzPredictorService(startHistory, _fixture.PredictionCollection[startHistory], _fixture.CommandCollection); + _telemetryClient = new MockAzPredictorTelemetryClient(); + _azPredictor = new AzPredictor(_service, _telemetryClient, new Settings() { SuggestionCount = 1, MaxAllowedCommandDuplicate = 1, @@ -51,84 +51,250 @@ public AzPredictorTests(ModelFixture modelFixture) } /// - /// Verifies when the last command in history are not supported. - /// We don't collect the telemetry and only request prediction while StartEarlyProcess is called. + /// Verify we replace unsupported command with . /// - [Theory] - [InlineData("start_of_snippet\nstart_of_snippet\nstart_of_snippet")] - [InlineData("start_of_snippet")] - [InlineData("")] - [InlineData("git status")] - [InlineData("git status\nGet-ChildItem")] - [InlineData("^29a9l2")] - [InlineData("'Get-AzResource'")] - [InlineData("Get-AzResource\ngit log")] - [InlineData("Get-ChildItem")] - public void VerifyWithNonSupportedCommand(string historyLine) + [Fact] + public void VerifyRequestPredictionForOneUnsupportedCommandInHistory() { - IReadOnlyList history = historyLine.Split('\n'); + IReadOnlyList history = new List() + { + "git status" + }; - this._telemetryClient.RecordedSuggestion = null; - this._service.IsPredictionRequested = false; + _telemetryClient.RecordedSuggestion = null; + _service.Commands = null; + _service.History = null; - this._azPredictor.StartEarlyProcessing(history); + _azPredictor.StartEarlyProcessing(history); - Assert.True(this._service.IsPredictionRequested); - Assert.NotNull(this._telemetryClient.RecordedSuggestion); + Assert.Equal(new List() { AzPredictorConstants.CommandPlaceholder, AzPredictorConstants.CommandPlaceholder }, _service.Commands); + Assert.Equal(AzPredictorConstants.CommandPlaceholder, _telemetryClient.RecordedSuggestion.HistoryLine); + Assert.Null(_service.History); } /// - /// Verifies when the last command in history are not supported. - /// We don't collect the telemetry and only request prediction while StartEarlyProcess is called. + /// Verify that we masked the supported command in requesting prediction and telemetry. /// - [Theory] - [InlineData("start_of_snippet\nConnect-AzAccount")] - [InlineData("Get-AzResource")] - [InlineData("git status\nGet-AzContext")] - [InlineData("Get-AzContext\nGet-AzLog")] - public void VerifyWithOneSupportedCommand(string historyLine) + [Fact] + public void VerifyRequestPredictionForOneSupportedCommandInHistory() + { + IReadOnlyList history = new List() + { + "New-AzVM -Name hello -Location WestUS" + }; + + _telemetryClient.RecordedSuggestion = null; + _service.Commands = null; + _service.History = null; + + _azPredictor.StartEarlyProcessing(history); + + string maskedCommand = "New-AzVM -Location *** -Name ***"; + + Assert.Equal(new List() { AzPredictorConstants.CommandPlaceholder, maskedCommand }, _service.Commands); + Assert.Equal(maskedCommand, _telemetryClient.RecordedSuggestion.HistoryLine); + Assert.Equal(history[0], _service.History.ToString()); + } + + /// + /// Verify that we can handle the two supported command in sequences. + /// + [Fact] + public void VerifyRequestPredictionForTwoSupportedCommandInHistory() + { + IReadOnlyList history = new List() + { + "New-AzResourceGroup -Name 'resourceGroup01'", + "New-AzVM -Name:hello -Location:WestUS" + }; + + _telemetryClient.RecordedSuggestion = null; + _service.Commands = null; + _service.History = null; + + _azPredictor.StartEarlyProcessing(history); + + var maskedCommands = new List() + { + "New-AzResourceGroup -Name ***", + "New-AzVM -Location:*** -Name:***" + }; + + Assert.Equal(maskedCommands, _service.Commands); + Assert.Equal(maskedCommands[1], _telemetryClient.RecordedSuggestion.HistoryLine); + Assert.Equal(history[1], _service.History.ToString()); + } + + /// + /// Verify that we can handle the two unsupported command in sequences. + /// + [Fact] + public void VerifyRequestPredictionForTwoUnsupportedCommandInHistory() + { + IReadOnlyList history = new List() + { + "git status", + @"$a='ResourceGroup01'", + }; + + _telemetryClient.RecordedSuggestion = null; + _service.Commands = null; + _service.History = null; + + _azPredictor.StartEarlyProcessing(history); + + var maskedCommands = new List() + { + AzPredictorConstants.CommandPlaceholder, + AzPredictorConstants.CommandPlaceholder, + }; + + Assert.Equal(maskedCommands, _service.Commands); + Assert.Equal(maskedCommands[1], _telemetryClient.RecordedSuggestion.HistoryLine); + Assert.Null(_service.History); + } + + /// + /// Verify that we skip the unsupported commands. + /// + [Fact] + public void VerifyNotTakeUnsupportedCommands() + { + var history = new List() + { + "New-AzResourceGroup -Name:resourceGroup01", + "New-AzVM -Name hello -Location WestUS" + }; + + _telemetryClient.RecordedSuggestion = null; + _service.Commands = null; + _service.History = null; + + _azPredictor.StartEarlyProcessing(history); + + history.Add("git status"); + _azPredictor.StartEarlyProcessing(history); + + history.Add(@"$a='NewResourceName'"); + _azPredictor.StartEarlyProcessing(history); + + // We don't take the last two unsupported command to request predictions. + // But we send the masked one in telemetry. + + var maskedCommands = new List() + { + "New-AzResourceGroup -Name:***", + "New-AzVM -Location *** -Name ***" + }; + + Assert.Equal(maskedCommands, _service.Commands); + Assert.Equal(AzPredictorConstants.CommandPlaceholder, _telemetryClient.RecordedSuggestion.HistoryLine); + Assert.Equal(history[1], _service.History.ToString()); + + // When there is a new supported command, we'll use that for prediction. + + history.Add("Get-AzResourceGroup -Name ResourceGroup01"); + _azPredictor.StartEarlyProcessing(history); + + maskedCommands = new List() + { + "New-AzVM -Location *** -Name ***", + "Get-AzResourceGroup -Name ***", + }; + + Assert.Equal(maskedCommands, _service.Commands); + Assert.Equal(maskedCommands[1], _telemetryClient.RecordedSuggestion.HistoryLine); + Assert.Equal(history.Last(), _service.History.ToString()); + } + + /// + /// Verify that we handle the three supported command in the same order. + /// + [Fact] + public void VerifyThreeSupportedCommands() { - IReadOnlyList history = historyLine.Split('\n'); + var history = new List() + { + "New-AzResourceGroup -Name resourceGroup01", + "New-AzVM -Name:hello -Location:WestUS" + }; - this._telemetryClient.RecordedSuggestion = null; - this._service.IsPredictionRequested = false; + _telemetryClient.RecordedSuggestion = null; + _service.Commands = null; + _service.History = null; - this._azPredictor.StartEarlyProcessing(history); + _azPredictor.StartEarlyProcessing(history); - Assert.True(this._service.IsPredictionRequested); - Assert.NotNull(this._telemetryClient.RecordedSuggestion); + history.Add("Get-AzResourceGroup -Name resourceGroup01"); + _azPredictor.StartEarlyProcessing(history); + + var maskedCommands = new List() + { + "New-AzVM -Location:*** -Name:***", + "Get-AzResourceGroup -Name ***", + }; + + Assert.Equal(maskedCommands, _service.Commands); + Assert.Equal(maskedCommands[1], _telemetryClient.RecordedSuggestion.HistoryLine); + Assert.Equal(history.Last(), _service.History.ToString()); } /// - /// Verify that the supported commands parameter values are masked. + /// Verify that we handle the sequence of one unsupported command and one supported command. /// [Fact] - public void VerifySupportedCommandMasked() + public void VerifyUnsupportedAndSupportedCommands() { - var input = "Get-AzVMExtension -ResourceGroupName 'ResourceGroup11' -VMName 'VirtualMachine22'"; - var expected = "Get-AzVMExtension -ResourceGroupName *** -VMName ***"; + var history = new List() + { + "git status", + "New-AzVM -Name:hello -Location:WestUS" + }; - this._telemetryClient.RecordedSuggestion = null; - this._service.IsPredictionRequested = false; + _telemetryClient.RecordedSuggestion = null; + _service.Commands = null; + _service.History = null; - this._azPredictor.StartEarlyProcessing(new List { input } ); + _azPredictor.StartEarlyProcessing(history); - Assert.True(this._service.IsPredictionRequested); - Assert.NotNull(this._telemetryClient.RecordedSuggestion); - Assert.Equal(expected, this._telemetryClient.RecordedSuggestion.HistoryLine); + var maskedCommands = new List() + { + AzPredictorConstants.CommandPlaceholder, + "New-AzVM -Location:*** -Name:***" + }; - input = "Get-AzStorageAccountKey -Name:'ContosoStorage' -ResourceGroupName:'ContosoGroup02'"; - expected = "Get-AzStorageAccountKey -Name:*** -ResourceGroupName:***"; + Assert.Equal(maskedCommands, _service.Commands); + Assert.Equal(maskedCommands[1], _telemetryClient.RecordedSuggestion.HistoryLine); + Assert.Equal(history.Last(), _service.History.ToString()); + } + /// + /// Verify that we handle the sequence of one supported command and one unsupported command. + /// + [Fact] + public void VerifySupportedAndUnsupportedCommands() + { + var history = new List() + { + "New-AzVM -Name hello -Location WestUS", + "git status", + }; + + _telemetryClient.RecordedSuggestion = null; + _service.Commands = null; + _service.History = null; - this._telemetryClient.RecordedSuggestion = null; - this._service.IsPredictionRequested = false; + _azPredictor.StartEarlyProcessing(history); - this._azPredictor.StartEarlyProcessing(new List { input } ); + var maskedCommands = new List() + { + AzPredictorConstants.CommandPlaceholder, + "New-AzVM -Location *** -Name ***", + }; - Assert.True(this._service.IsPredictionRequested); - Assert.NotNull(this._telemetryClient.RecordedSuggestion); - Assert.Equal(expected, this._telemetryClient.RecordedSuggestion.HistoryLine); + Assert.Equal(maskedCommands, _service.Commands); + Assert.Equal(AzPredictorConstants.CommandPlaceholder, _telemetryClient.RecordedSuggestion.HistoryLine); + Assert.Equal(history.First(), _service.History.ToString()); } /// @@ -140,8 +306,8 @@ public void VerifySupportedCommandMasked() public void VerifySuggestion(string userInput) { var predictionContext = PredictionContext.Create(userInput); - var expected = this._service.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None); - var actual = this._azPredictor.GetSuggestion(predictionContext, CancellationToken.None); + var expected = _service.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None); + var actual = _azPredictor.GetSuggestion(predictionContext, CancellationToken.None); Assert.Equal(expected.Count, actual.Count); Assert.Equal(expected.PredictiveSuggestions.First().SuggestionText, actual.First().SuggestionText); @@ -154,7 +320,7 @@ public void VerifySuggestion(string userInput) public void VerifySuggestionOnIncompleteCommand() { // We need to get the suggestions for more than one. So we create a local version az predictor. - var localAzPredictor = new AzPredictor(this._service, this._telemetryClient, new Settings() + var localAzPredictor = new AzPredictor(_service, _telemetryClient, new Settings() { SuggestionCount = 7, MaxAllowedCommandDuplicate = 1, @@ -170,7 +336,6 @@ public void VerifySuggestionOnIncompleteCommand() Assert.Equal(expected, actual.First().SuggestionText); } - /// /// Verify when we cannot parse the user input correctly. /// @@ -183,7 +348,7 @@ public void VerifySuggestionOnIncompleteCommand() public void VerifyMalFormattedCommandLine(string userInput) { var predictionContext = PredictionContext.Create(userInput); - var actual = this._azPredictor.GetSuggestion(predictionContext, CancellationToken.None); + var actual = _azPredictor.GetSuggestion(predictionContext, CancellationToken.None); Assert.Empty(actual); } diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/Mocks/MockAzPredictorService.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/Mocks/MockAzPredictorService.cs index 0d0d3d74d724..f431390cfe0f 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/Mocks/MockAzPredictorService.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/Mocks/MockAzPredictorService.cs @@ -12,8 +12,8 @@ // limitations under the License. // ---------------------------------------------------------------------------------- -using System; using System.Collections.Generic; +using System.Management.Automation.Language; namespace Microsoft.Azure.PowerShell.Tools.AzPredictor.Test.Mocks { @@ -23,9 +23,14 @@ namespace Microsoft.Azure.PowerShell.Tools.AzPredictor.Test.Mocks sealed class MockAzPredictorService : AzPredictorService { /// - /// Gets or sets if a predictions is requested. + /// Gets or sets the commands in history to request prediction for. /// - public bool IsPredictionRequested { get; set; } + public IEnumerable Commands { get; set; } + + /// + /// Gets or sets the commands that's recorded in history. + /// + public CommandAst History { get; set; } /// /// Constructs a new instance of @@ -52,9 +57,9 @@ public MockAzPredictorService(string history, IList suggestio } /// - public override void RequestPredictions(IEnumerable history) + public override void RequestPredictions(IEnumerable commands) { - this.IsPredictionRequested = true; + Commands = commands; } /// @@ -62,5 +67,11 @@ protected override void RequestAllPredictiveCommands() { // Do nothing since we've set the command and suggestion predictors. } + + /// + public override void RecordHistory(CommandAst history) + { + History = history; + } } } diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs index a03eed3391a2..9fa881d3ed49 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs @@ -83,11 +83,21 @@ public void StartEarlyProcessing(IReadOnlyList history) if (history.Count > 0) { - if (_lastTwoMaskedCommands.Any()) - { - _lastTwoMaskedCommands.Dequeue(); - } - else + // We try to find the commands to request predictions for. + // We should only have "start_of_snippet" when there are no enough Az commands for prediction. + // We then ignore that when there are new "start_of_snippet". + // This is the scenario. + // 1. New-AzResourceGroup -Name **** + // 2. $resourceName="Test" + // 3. $resourceLocation="westus2" + // 4. New-AzVM -Name $resourceName -Location $resourceLocation + // + // We'll replace 2 and 3 with "start_of_snippet" but if we request prediction using 2 and 3, that'll reset the + // workflow. We want to predict only by Az commands. That's to use commands 1 and 4. + + bool isLastTwoCommandsChanged = false; + + if (_lastTwoMaskedCommands.Count == 0) { // This is the first time we populate our record. Push the second to last command in history to the // queue. If there is only one command in history, push the command placeholder. @@ -97,7 +107,11 @@ public void StartEarlyProcessing(IReadOnlyList history) string secondToLastLine = history.TakeLast(AzPredictorConstants.CommandHistoryCountToProcess).First(); var secondToLastCommand = GetAstAndMaskedCommandLine(secondToLastLine); _lastTwoMaskedCommands.Enqueue(secondToLastCommand.Item2); - _service.RecordHistory(secondToLastCommand.Item1); + + if (!string.Equals(AzPredictorConstants.CommandPlaceholder, secondToLastCommand.Item2, StringComparison.Ordinal)) + { + _service.RecordHistory(secondToLastCommand.Item1); + } } else { @@ -105,20 +119,41 @@ public void StartEarlyProcessing(IReadOnlyList history) // We only extract parameter values from the command line in _service.RecordHistory. // So we don't need to do that for a placeholder. } + + isLastTwoCommandsChanged = true; } string lastLine = history.Last(); var lastCommand = GetAstAndMaskedCommandLine(lastLine); + bool isLastCommandSupported = !string.Equals(AzPredictorConstants.CommandPlaceholder, lastCommand.Item2, StringComparison.Ordinal); - _lastTwoMaskedCommands.Enqueue(lastCommand.Item2); - - if ((lastCommand.Item2 != null) && !string.Equals(AzPredictorConstants.CommandPlaceholder, lastCommand.Item2, StringComparison.Ordinal)) + if (isLastCommandSupported) { + if (_lastTwoMaskedCommands.Count == 2) + { + // There are already two commands, dequeue the oldest one. + _lastTwoMaskedCommands.Dequeue(); + } + + _lastTwoMaskedCommands.Enqueue(lastCommand.Item2); + isLastTwoCommandsChanged = true; + _service.RecordHistory(lastCommand.Item1); } + else if (_lastTwoMaskedCommands.Count == 1) + { + isLastTwoCommandsChanged = true; + var existingInQueue = _lastTwoMaskedCommands.Dequeue(); + _lastTwoMaskedCommands.Enqueue(AzPredictorConstants.CommandPlaceholder); + _lastTwoMaskedCommands.Enqueue(existingInQueue); + } _telemetryClient.OnHistory(new HistoryTelemetryData(lastCommand.Item2)); - _service.RequestPredictions(_lastTwoMaskedCommands); + + if (isLastTwoCommandsChanged) + { + _service.RequestPredictions(_lastTwoMaskedCommands); + } } ValueTuple GetAstAndMaskedCommandLine(string commandLine) @@ -126,7 +161,7 @@ ValueTuple GetAstAndMaskedCommandLine(string commandLine) var asts = Parser.ParseInput(commandLine, out _, out _); var allNestedAsts = asts?.FindAll((ast) => ast is CommandAst, true); var commandAst = allNestedAsts?.LastOrDefault() as CommandAst; - string maskedCommandLine = null; + string maskedCommandLine = AzPredictorConstants.CommandPlaceholder; var commandName = commandAst?.CommandElements?.FirstOrDefault().ToString(); @@ -134,10 +169,6 @@ ValueTuple GetAstAndMaskedCommandLine(string commandLine) { maskedCommandLine = CommandLineUtilities.MaskCommandLine(commandAst); } - else - { - maskedCommandLine = AzPredictorConstants.CommandPlaceholder; - } return ValueTuple.Create(commandAst, maskedCommandLine); }