Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions samples/pause/Pause_testPlan.fx.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
testSuite:
testSuiteName: Pause Function Tests
testSuiteDescription: Verifies that the Pause function works correctly
persona: User1
appLogicalName: mda_input_controls_app

testCases:
- testCaseName: Test Pause Function - Non-Headless Mode
testCaseDescription: Tests that Pause function works when headless is false
testSteps: |
=
Screenshot("before_pause.png");
Pause();
Screenshot("after_pause.png");
Assert(true, "Test continued after Pause function");

testSettings:
headless: false
browserConfigurations:
- browser: Chromium
channel: msedge
extensionModules:
enable: true
allowPowerFxNamespaces:
- Preview

environmentVariables:
users:
- personaName: User1
emailKey: user1Email
passwordKey: NotNeeded
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ public void IsValidUserModule(string allow, string deny, string providerNamespac
%CODE%";

[Theory]
[InlineData("Test", "", "public class FooFunction : ReflectionFunction { public FooFunction() : base(\"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", false)] // No namespace
[InlineData("Test", "", "public class FooFunction : ReflectionFunction { public FooFunction() : base(\"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", true)] // TestEngine namespace (default for functions without DPath)
[InlineData("Test", "", "public class FooFunction : ReflectionFunction { public FooFunction() : base(DPath.Root, \"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", false)] // Root namespace
[InlineData("Test", "", "public class FooFunction : ReflectionFunction { public FooFunction() : base(DPath.Root.Append(new DName(\"Test\")), \"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", true)] // Non Root namespace
[InlineData("", "", "public class FooFunction : ReflectionFunction { public FooFunction() : base(DPath.Root.Append(new DName(\"TestEngine\")), \"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", true)] // Allow TestEngine namespace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,12 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s
stream.Position = 0;
ModuleDefinition module = ModuleDefinition.ReadModule(stream);

// Detect if this assembly contains a provider/user/auth implementation
bool assemblyHasProvider = module.GetAllTypes().Any(t =>
t.Interfaces.Any(i => i.InterfaceType.FullName == typeof(Providers.ITestWebProvider).FullName) ||
t.Interfaces.Any(i => i.InterfaceType.FullName == typeof(Users.IUserManager).FullName) ||
t.Interfaces.Any(i => i.InterfaceType.FullName == typeof(Config.IUserCertificateProvider).FullName));

// Get the source code of the assembly as will be used to check Power FX Namespaces
var code = DecompileModuleToCSharp(assembly);

Expand All @@ -352,6 +358,12 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s
{
foreach (var name in values)
{
// Ignore Preview namespace for provider loading if not explicitly allowed to avoid blocking provider registration in Release builds.
if (name == NAMESPACE_PREVIEW && !settings.AllowPowerFxNamespaces.Contains(NAMESPACE_PREVIEW))
{
Logger.LogInformation($"Ignoring Preview namespace on provider {type.Name} (not enabled in allow list).");
continue;
}
// Check against deny list using regular expressions
if (settings.DenyPowerFxNamespaces.Any(pattern => Regex.IsMatch(name, WildcardToRegex(pattern))))
{
Expand Down Expand Up @@ -389,68 +401,73 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s
Logger.LogInformation($"No constructor defined for {type.Name}. Found {constructors.Count()} expected 1 or more");
return false;
}

var constructor = constructors.Where(c => c.HasBody).FirstOrDefault();

if (constructor == null)
var constructor = constructors.FirstOrDefault(c => c.HasBody);
if (constructor == null || !constructor.HasBody)
{
Logger.LogInformation($"No constructor with a body");
Logger.LogInformation($"No constructor with a body for {type.Name}");
return false;
}

if (!constructor.HasBody)
if (!constructor.IsPublic)
{
Logger.LogInformation($"No body defined for {type.Name}");
// Needs body for call to base constructor
Logger.LogInformation($"Constructor must be public for {type.Name}");
return false;
}

var baseCall = constructor.Body.Instructions?.FirstOrDefault(i => i.OpCode == OpCodes.Call && i.Operand is MethodReference && ((MethodReference)i.Operand).Name == ".ctor");

var baseCall = constructor.Body.Instructions?.FirstOrDefault(i => i.OpCode == OpCodes.Call && i.Operand is MethodReference mr && mr.Name == ".ctor");
if (baseCall == null)
{
Logger.LogInformation($"No base constructor defined for {type.Name}");
// Unable to find base constructor call
return false;
}

MethodReference baseConstructor = (MethodReference)baseCall.Operand;

var baseConstructor = (MethodReference)baseCall.Operand;
if (baseConstructor.Parameters?.Count() < 2)
{
// Not enough parameters
Logger.LogInformation($"No not enough parameters for {type.Name}");
return false;
}

if (baseConstructor.Parameters[0].ParameterType.FullName != "Microsoft.PowerFx.Core.Utils.DPath")
string name;
var hasNamespaceParam = baseConstructor.Parameters[0].ParameterType.FullName == "Microsoft.PowerFx.Core.Utils.DPath";
if (hasNamespaceParam)
{
// First argument should be Namespace
Logger.LogInformation($"No Power FX Namespace for {type.Name}");
return false;
name = GetPowerFxNamespace(type.Name, code);
if (string.IsNullOrEmpty(name))
{
Logger.LogInformation($"No Power FX Namespace found for {type.Name}");
return false;
}
}
else
{
// Root/global function (e.g. Pause). Infer via attribute if needed.
bool isPreview = false, isDeprecated = false;
foreach (var ca in type.CustomAttributes)
{
if (ca.AttributeType.FullName == "Microsoft.PowerApps.TestEngine.Modules.TestEngineFunctionAttribute")
{
foreach (var p in ca.Properties)
{
if (p.Name == "IsPreview" && p.Argument.Value is bool pv) isPreview = pv;
if (p.Name == "IsDeprecated" && p.Argument.Value is bool dp) isDeprecated = dp;
}
}
}
name = isPreview ? NAMESPACE_PREVIEW : isDeprecated ? NAMESPACE_DEPRECATED : NAMESPACE_TEST_ENGINE;
}

// Use the decompiled code to get the values of the base constructor, specifically look for the namespace
var name = GetPowerFxNamespace(type.Name, code);

if (string.IsNullOrEmpty(name))
// Simple optimization: if this is a provider assembly and Preview not enabled, silently skip Preview functions.
if (assemblyHasProvider && name == NAMESPACE_PREVIEW && !settings.AllowPowerFxNamespaces.Contains(NAMESPACE_PREVIEW))
{
// No Power FX Namespace found
Logger.LogInformation($"No Power FX Namespace found for {type.Name}");
return false;
Logger.LogInformation($"Skipping Preview validation for provider function {type.Name} (Preview not enabled).");
continue;
}

if (settings.DenyPowerFxNamespaces.Contains(name))
{
// Deny list match
Logger.LogInformation($"Deny Power FX Namespace {name} for {type.Name}");
return false;
}

if ((settings.DenyPowerFxNamespaces.Contains("*") && (
!settings.AllowPowerFxNamespaces.Contains(name) ||
(!settings.AllowPowerFxNamespaces.Contains(name) && name != NAMESPACE_TEST_ENGINE)
)
))
if (settings.DenyPowerFxNamespaces.Contains("*") && !settings.AllowPowerFxNamespaces.Contains(name) && name != NAMESPACE_TEST_ENGINE)
{
// Deny wildcard exists only. Could not find match in allow list and name was not reserved name TestEngine
Logger.LogInformation($"Deny Power FX Namespace {name} for {type.Name}");
Expand Down
4 changes: 2 additions & 2 deletions src/testengine.module.pause.tests/PauseFunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void PauseExecute()
// Act
module.Execute();

// Assert
// Assert - Should execute successfully
MockLogger.Verify(l => l.Log(It.Is<LogLevel>(l => l == LogLevel.Information),
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString() == "Successfully finished executing Pause function."),
Expand Down Expand Up @@ -94,7 +94,7 @@ public void SkipExecute()
// Act
module.Execute();

// Assert
// Assert - Should skip in headless mode
MockLogger.Verify(l => l.Log(It.Is<LogLevel>(l => l == LogLevel.Information),
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString() == "Skip Pause function as in headless mode."),
Expand Down
36 changes: 35 additions & 1 deletion src/testengine.module.pause.tests/PauseModuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,41 @@ public void RegisterPowerFxFunction()
// Act
module.RegisterPowerFxFunction(TestConfig, MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object);

// Assert
// Assert - Should skip registration since Preview is not enabled by default
MockLogger.Verify(l => l.Log(It.Is<LogLevel>(l => l == LogLevel.Information),
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString() == "Skip registering Pause() - Preview namespace not enabled"),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.AtLeastOnce);
}

[Fact]
public void RegisterPowerFxFunctionWithPreviewEnabled()
{
// Arrange
var module = new PauseModule();
var settings = new TestSettings()
{
ExtensionModules = new TestSettingExtensions()
{
AllowPowerFxNamespaces = new HashSet<string> { "Preview" }
}
};

MockTestState.Setup(x => x.GetTestSettings()).Returns(settings);
MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object);

MockLogger.Setup(x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));

// Act
module.RegisterPowerFxFunction(TestConfig, MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object);

// Assert - Should register since Preview is enabled
MockLogger.Verify(l => l.Log(It.Is<LogLevel>(l => l == LogLevel.Information),
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString() == "Registered Pause()"),
Expand Down
6 changes: 2 additions & 4 deletions src/testengine.module.pause/PauseFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Microsoft.PowerApps.TestEngine.Config;
using Microsoft.PowerApps.TestEngine.TestInfra;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Core.Utils;
using Microsoft.PowerFx.Types;

namespace testengine.module
Expand All @@ -20,7 +19,7 @@ public class PauseFunction : ReflectionFunction
private readonly ILogger _logger;

public PauseFunction(ITestInfraFunctions testInfraFunctions, ITestState testState, ILogger logger)
: base(DPath.Root.Append(new DName("Preview")), "Pause", FormulaType.Blank)
: base("Pause", FormulaType.Blank)
{
_testInfraFunctions = testInfraFunctions;
_testState = testState;
Expand All @@ -29,8 +28,7 @@ public PauseFunction(ITestInfraFunctions testInfraFunctions, ITestState testStat

public BlankValue Execute()
{
_logger.LogInformation("------------------------------\n\n" +
"Executing Pause function.");
_logger.LogInformation("------------------------------\n\nExecuting Pause function.");

if (!_testState.GetTestSettings().Headless)
{
Expand Down
39 changes: 36 additions & 3 deletions src/testengine.module.pause/PauseModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,54 @@ namespace testengine.module
[Export(typeof(ITestEngineModule))]
public class PauseModule : ITestEngineModule
{
/// <summary>
/// Indicates whether Preview namespace is enabled in YAML testSettings.extensionModules.allowPowerFxNamespaces.
/// </summary>
public virtual bool IsPreviewNamespaceEnabled { get; private set; } = false;

public void ExtendBrowserContextOptions(BrowserNewContextOptions options, TestSettings settings)
{

UpdatePreviewNamespaceProperty(settings);
}

public void RegisterPowerFxFunction(PowerFxConfig config, ITestInfraFunctions testInfraFunctions, ITestWebProvider testWebProvider, ISingleTestInstanceState singleTestInstanceState, ITestState testState, IFileSystem fileSystem)
{
TestSettings testSettings = null;
try
{
if (testState != null)
{
testSettings = testState.GetTestSettings();
}
}
catch
{
testSettings = null;
}
UpdatePreviewNamespaceProperty(testSettings);

ILogger logger = singleTestInstanceState.GetLogger();
config.AddFunction(new PauseFunction(testInfraFunctions, testState, logger));
logger.LogInformation("Registered Pause()");

// Only register Pause() function if Preview namespace is enabled
if (IsPreviewNamespaceEnabled)
{
config.AddFunction(new PauseFunction(testInfraFunctions, testState, logger));
logger.LogInformation("Registered Pause()");
}
else
{
logger.LogInformation("Skip registering Pause() - Preview namespace not enabled");
}
}

public async Task RegisterNetworkRoute(ITestState state, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, IPage Page, NetworkRequestMock mock)
{
await Task.CompletedTask;
}

private void UpdatePreviewNamespaceProperty(TestSettings settings)
{
IsPreviewNamespaceEnabled = settings?.ExtensionModules?.AllowPowerFxNamespaces?.Contains("Preview") ?? false;
}
}
}
Loading