Skip to content

Commit 74229e8

Browse files
committed
updating scripts to try this out in consolidated pipeline
1 parent 0c0f576 commit 74229e8

29 files changed

+194
-148
lines changed

Directory.Build.props

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<RepoRoot>$(MSBuildThisFileDirectory)</RepoRoot>
55
<RepoEngRoot>$(RepoRoot)eng/</RepoEngRoot>
66
<RepoSrcRoot>$(RepoRoot)src/</RepoSrcRoot>
7+
<RepoTestRoot>$(RepoRoot)test/</RepoTestRoot>
78
</PropertyGroup>
89

910
<!-- artifacts -->

eng/scripts/ArtifactAssemblerHelpers/testArtifacts.ps1

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ Write-Host "$rootDir"
88
ls $rootDir
99

1010
# Set the path to test project (.csproj) and runtime settings
11-
$testProjectPath = ".\test\Cli\Func.E2ETests\Func.E2ETests.csproj"
12-
$defaultRuntimeSettings = ".\test\Cli\Func.E2ETests\Runsettings\StartTests_artifact_consolidation.runsettings"
13-
$inProcRuntimeSettings = ".\test\Cli\Func.E2ETests\Runsettings\StartTests_dotnet_inproc_artifact_consolidation.runsettings"
11+
$testProjectPath = ".\test\Cli\Func.E2E.Tests\Azure.Functions.Cli.E2E.Tests.csproj"
12+
$defaultRuntimeSettings = ".\test\Cli\Func.E2E.Tests\.runsettings\start_tests\artifact_consolidation_pipeline\default.runsettings"
13+
$inProcRuntimeSettings = ".\test\Cli\Func.E2E.Tests\.runsettings\start_tests\artifact_consolidation_pipeline\dotnet_inproc.runsettings"
1414

1515
dotnet build $testProjectPath
1616

eng/scripts/ArtifactAssemblerHelpers/testVsArtifacts.ps1

+5-8
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,14 @@ Write-Host "Root directory: $rootDir"
88
ls $rootDir
99

1010
# Set the path to test project (.csproj) and runtime settings
11-
$testProjectPath = ".\test\Cli\Func.E2ETests\Func.E2ETests.csproj"
12-
$runtimeSettings = ".\test\Cli\Func.E2ETests\Runsettings\StartTests_artifact_consolidation_visualstudio.runsettings"
11+
$testProjectPath = ".\test\Cli\Func.E2E.Tests\Azure.Functions.Cli.E2E.Tests.csproj"
12+
$runtimeSettings = ".\test\Cli\Func.E2E.Tests\.runsettings\start_tests\artifact_consolidation_pipeline\visualstudio.runsettings"
1313

1414
[System.Environment]::SetEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "dotnet", "Process")
1515

16-
# Build the test project
17-
Write-Host "Building test project: $absoluteTestProjectPath"
18-
dotnet build $absoluteTestProjectPath
19-
2016
# Path for Visual Studio test projects (convert to absolute paths)
21-
$net8VsProjectPath = ".\test\Cli\Func.E2ETests\VisualStudioTestProjects\TestNet8InProcProject"
22-
$net6VsProjectPath = ".\test\Cli\Func.E2ETests\VisualStudioTestProjects\TestNet6InProcProject"
17+
$net8VsProjectPath = ".\test\TestFunctionApps\VisualStudioTestProjects\TestNet8InProcProject"
18+
$net6VsProjectPath = ".\test\TestFunctionApps\VisualStudioTestProjects\TestNet6InProcProject"
2319

2420
# Resolve paths to absolute paths
2521
$absoluteNet8VsProjectPath = (Resolve-Path -Path $net8VsProjectPath -ErrorAction SilentlyContinue).Path
@@ -38,6 +34,7 @@ if (-not $absoluteNet6VsProjectPath) {
3834
Write-Host "Absolute NET6 VS project path (resolved): $absoluteNet6VsProjectPath"
3935
}
4036

37+
# Build the test project
4138
dotnet build $testProjectPath
4239

4340
# Loop through each subdirectory within the parent directory

src/Cli/func/Common/Constants.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace Azure.Functions.Cli.Common
77
{
8-
public static class Constants
8+
internal static class Constants
99
{
1010
public const string StorageConnectionStringTemplate = "DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}";
1111
public const string FunctionsStorageAccountNamePrefix = "AzureFunctions";
+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Runtime.CompilerServices;
22

33
[assembly: InternalsVisibleToAttribute("Azure.Functions.Cli.Tests")]
4-
[assembly: InternalsVisibleToAttribute("DynamicProxyGenAssembly2")]
4+
[assembly: InternalsVisibleToAttribute("DynamicProxyGenAssembly2")]
5+
[assembly: InternalsVisibleToAttribute("Azure.Functions.Cli.E2E.Tests")]

test/Cli/Func.E2E.Tests/Azure.Functions.Cli.E2E.Tests.csproj

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
5+
<DisableImplicitNamespaceImports>true</DisableImplicitNamespaceImports>
56
</PropertyGroup>
67

78
<ItemGroup>
@@ -23,11 +24,11 @@
2324

2425
<ItemGroup>
2526
<ProjectReference Include="$(RepoSrcRoot)Cli\func\Azure.Functions.Cli.csproj" />
26-
<ProjectReference Include="..\TestFramework\Azure.Functions.Cli.TestFramework.csproj" />
27+
<ProjectReference Include="$(RepoTestRoot)Cli\TestFramework\Azure.Functions.Cli.TestFramework.csproj" />
2728
</ItemGroup>
2829

2930
<PropertyGroup>
30-
<RunSettingsFilePath>$(MSBuildProjectDirectory)\.runsettings\start_tests\ci_pipeline\default.runsettings</RunSettingsFilePath>
31+
<RunSettingsFilePath>$(MSBuildProjectDirectory)\.runsettings\start_tests\ci_pipeline\default.runsettings</RunSettingsFilePath>
3132
</PropertyGroup>
3233

3334
</Project>

test/Cli/Func.E2E.Tests/Commands/FuncStart/AuthTests.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,25 @@ public class AuthTests(ITestOutputHelper log) : BaseE2ETests(log)
1616
[InlineData("function", false, "Welcome to Azure Functions!")]
1717
[InlineData("function", true, "")]
1818
[InlineData("anonymous", true, "Welcome to Azure Functions!")]
19-
public async Task Start_DotnetIsolated_Test_EnableAuthFeature(
19+
public async Task Start_DotnetIsolated_EnableAuthFeature(
2020
string authLevel,
2121
bool enableAuth,
2222
string expectedResult)
2323
{
2424
var port = ProcessHelper.GetAvailablePort();
2525

26-
var methodName = nameof(Start_DotnetIsolated_Test_EnableAuthFeature);
26+
var methodName = nameof(Start_DotnetIsolated_EnableAuthFeature);
2727
var uniqueTestName = $"{methodName}_{authLevel}_{enableAuth}";
2828

2929
// Call func init and func new
30-
await FuncInitWithRetryAsync(uniqueTestName, new[] { ".", "--worker-runtime", "dotnet-isolated" });
31-
await FuncNewWithRetryAsync(uniqueTestName, new[] { ".", "--template", "Httptrigger", "--name", "HttpTrigger", "--authlevel", authLevel });
30+
await FuncInitWithRetryAsync(uniqueTestName, [".", "--worker-runtime", "dotnet-isolated"]);
31+
await FuncNewWithRetryAsync(uniqueTestName, [".", "--template", "Httptrigger", "--name", "HttpTrigger", "--authlevel", authLevel]);
3232

3333
// Call func start
3434
var funcStartCommand = new FuncStartCommand(FuncPath, methodName, Log);
3535
funcStartCommand.ProcessStartedHandler = async (process) =>
3636
{
37-
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger");
37+
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand)), "HttpTrigger");
3838
};
3939

4040
// Build command arguments based on enableAuth parameter
@@ -55,7 +55,7 @@ public async Task Start_DotnetIsolated_Test_EnableAuthFeature(
5555
}
5656
else
5757
{
58-
result.Should().HaveStdOutContaining("Selected out-of-process host.");
58+
result.Should().StartDefaultHost();
5959
}
6060
}
6161
}

test/Cli/Func.E2E.Tests/Commands/FuncStart/Core/BaseMissingConfigTests.cs

+11-3
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public async Task RunMissingHostJsonTest(string language, string testName)
5858
result.Should().HaveStdOutContaining("Host.json file in missing");
5959
}
6060

61-
public async Task RunMissingLocalSettingsJsonTest(string language, string runtimeParameter, string expectedOutput, bool invokeFunction, bool setRuntimeViaEnvironment, string testName)
61+
public async Task RunMissingLocalSettingsJsonTest(string language, string runtimeParameter, string expectedOutput, bool invokeFunction, bool setRuntimeViaEnvironment, string testName, bool shouldWaitForHost = true)
6262
{
6363
try
6464
{
@@ -89,7 +89,16 @@ public async Task RunMissingLocalSettingsJsonTest(string language, string runtim
8989

9090
funcStartCommand.ProcessStartedHandler = async (process) =>
9191
{
92-
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTriggerFunc");
92+
// Wait for host to start up if param is set, otherwise just wait 5 seconds for logs and kill the process
93+
if (shouldWaitForHost)
94+
{
95+
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTriggerFunc");
96+
}
97+
else
98+
{
99+
await Task.Delay(5000);
100+
process.Kill(true);
101+
}
93102
};
94103

95104
var startCommand = new List<string> { "--port", port.ToString(), "--verbose" };
@@ -108,7 +117,6 @@ public async Task RunMissingLocalSettingsJsonTest(string language, string runtim
108117
result.Should().HaveStdOutContaining("HttpTriggerFunc: [GET,POST] http://localhost:");
109118
}
110119

111-
result.Should().HaveStdOutContaining("Executed 'Functions.HttpTriggerFunc' (Succeeded");
112120
result.Should().HaveStdOutContaining(expectedOutput);
113121
}
114122
finally

test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/InProcTestWithoutTargetFramework.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4+
using Azure.Functions.Cli.E2E.Tests.Traits;
45
using Azure.Functions.Cli.TestFramework.Assertions;
56
using Azure.Functions.Cli.TestFramework.Commands;
67
using Azure.Functions.Cli.TestFramework.Helpers;
78
using FluentAssertions;
8-
using Func.E2ETests.Traits;
99
using Xunit;
1010
using Xunit.Abstractions;
1111

test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/LogLevelTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

44
using Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core;
5-
using Func.E2ETests.Traits;
5+
using Azure.Functions.Cli.E2E.Tests.Traits;
66
using Xunit;
77
using Xunit.Abstractions;
88

test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/MissingConfigTests.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

44
using Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core;
5-
using Func.E2ETests.Traits;
5+
using Azure.Functions.Cli.E2E.Tests.Traits;
66
using Xunit;
77
using Xunit.Abstractions;
88

@@ -24,11 +24,11 @@ public async Task Start_InProc_MissingHostJson_FailsWithExpectedError()
2424
}
2525

2626
[Theory]
27-
[InlineData("dotnet", "--worker-runtime None", $"Use the up/down arrow keys to select a worker runtime:", false, false)] // Runtime parameter set to None, worker runtime prompt displayed
28-
[InlineData("dotnet", "", $"Use the up/down arrow keys to select a worker runtime:", false, false)] // Runtime parameter not provided, worker runtime prompt displayed
29-
public async Task Start_InProc_MissingLocalSettingsJson_BehavesAsExpected(string language, string runtimeParameter, string expectedOutput, bool invokeFunction, bool setRuntimeViaEnvironment)
27+
[InlineData("dotnet", "--worker-runtime None", "Use the up/down arrow keys to select a worker runtime:", false, false, false)] // Runtime parameter set to None, worker runtime prompt displayed
28+
[InlineData("dotnet", "", $"Use the up/down arrow keys to select a worker runtime:", false, false, false)] // Runtime parameter not provided, worker runtime prompt displayed
29+
public async Task Start_InProc_MissingLocalSettingsJson_BehavesAsExpected(string language, string runtimeParameter, string expectedOutput, bool invokeFunction, bool setRuntimeViaEnvironment, bool shouldWaitForHost)
3030
{
31-
await RunMissingLocalSettingsJsonTest(language, runtimeParameter, expectedOutput, invokeFunction, setRuntimeViaEnvironment, nameof(Start_InProc_MissingLocalSettingsJson_BehavesAsExpected));
31+
await RunMissingLocalSettingsJsonTest(language, runtimeParameter, expectedOutput, invokeFunction, setRuntimeViaEnvironment, nameof(Start_InProc_MissingLocalSettingsJson_BehavesAsExpected), shouldWaitForHost);
3232
}
3333
}
3434
}

test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/UserSecretsTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

44
using Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core;
5-
using Func.E2ETests.Traits;
5+
using Azure.Functions.Cli.E2E.Tests.Traits;
66
using Xunit;
77
using Xunit.Abstractions;
88

test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/VisualStudioInProcTests.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4+
using Azure.Functions.Cli.E2E.Tests.Traits;
45
using Azure.Functions.Cli.TestFramework.Assertions;
56
using Azure.Functions.Cli.TestFramework.Commands;
67
using Azure.Functions.Cli.TestFramework.Helpers;
78
using FluentAssertions;
8-
using Func.E2ETests.Traits;
99
using Xunit;
1010
using Xunit.Abstractions;
1111

@@ -14,8 +14,8 @@ namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.InProcTests
1414
[Trait(TestTraits.Group, TestTraits.UseInVisualStudioConsolidatedArtifactGeneration)]
1515
public class VisualStudioInProcTests(ITestOutputHelper log) : BaseE2ETests(log)
1616
{
17-
private readonly string _vsNet8ProjectPath = Path.Combine("..", "..", "..", "TestFunctionApps", "VisualStudioTestProjects", "TestNet8InProcProject");
18-
private readonly string _vsNet6ProjectPath = Path.Combine("..", "..", "..", "TestFunctionApps", "VisualStudioTestProjects", "TestNet6InProcProject");
17+
private readonly string _vsNet8ProjectPath = Environment.GetEnvironmentVariable(Constants.VisualStudioNet8ProjectPath) ?? Path.Combine("..", "..", "..", "..", "..", "TestFunctionApps", "VisualStudioTestProjects", "TestNet8InProcProject");
18+
private readonly string _vsNet6ProjectPath = Environment.GetEnvironmentVariable(Constants.VisualStudioNet6ProjectPath) ?? Path.Combine("..", "..", "..", "..", "..", "TestFunctionApps", "VisualStudioTestProjects", "TestNet6InProcProject");
1919

2020
[Fact]
2121
public void Start_InProc_Net8_VisualStudio_SuccessfulFunctionExecution()
@@ -38,7 +38,7 @@ public void Start_InProc_Net8_VisualStudio_SuccessfulFunctionExecution()
3838
capturedOutput.Should().Be("Hello, Test. This HTTP triggered function executed successfully.");
3939

4040
// Validate .NET 8 host was loaded
41-
result.Should().HaveStdOutContaining("Loading .NET 8 host");
41+
result.Should().LoadNet8HostVisualStudio();
4242
}
4343

4444
[Fact]
@@ -62,7 +62,7 @@ public void Start_InProc_Net6_VisualStudio_SuccessfulFunctionExecution()
6262
capturedOutput.Should().Be("Hello, Test. This HTTP triggered function executed successfully.");
6363

6464
// Validate .NET 6 host was loaded
65-
result.Should().HaveStdOutContaining("Loading .NET 6 host");
65+
result.Should().LoadNet6HostVisualStudio();
6666
}
6767
}
6868
}

test/Cli/Func.E2E.Tests/Commands/FuncStart/LogLevelTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public async Task Start_LanguageWorker_LogLevelOverridenViaSettings_LogLevelSetT
4747
var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log);
4848
funcStartCommand.ProcessStartedHandler = async (process) =>
4949
{
50-
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter, "HttpTrigger?name=Test");
50+
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test");
5151
};
5252
var result = funcStartCommand
5353
.WithWorkingDirectory(WorkingDirectory)
@@ -79,7 +79,7 @@ public async Task Start_LanguageWorker_LogLevelOverridenViaHostJson_LogLevelSetT
7979
var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log);
8080
funcStartCommand.ProcessStartedHandler = async (process) =>
8181
{
82-
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter, "HttpTrigger?name=Test");
82+
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test");
8383
};
8484
var result = funcStartCommand
8585
.WithWorkingDirectory(WorkingDirectory)

test/Cli/Func.E2E.Tests/Commands/FuncStart/MissingConfigTests.cs

+6-7
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Azure.Functions.Cli.TestFramework.Commands;
77
using Azure.Functions.Cli.TestFramework.Helpers;
88
using FluentAssertions;
9-
using Func.E2ETests.Traits;
109
using Xunit;
1110
using Xunit.Abstractions;
1211

@@ -15,15 +14,15 @@ namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart
1514
public class MissingConfigTests(ITestOutputHelper log) : BaseMissingConfigTests(log)
1615
{
1716
[Fact(Skip="Test fails and needs to be investiagted on why it does.")]
18-
public async Task Start_Dotnet_Isolated_InvalidHostJson_FailsWithExpectedError()
17+
public async Task Start_DotnetIsolated_InvalidHostJson_FailsWithExpectedError()
1918
{
20-
await RunInvalidHostJsonTest("dotnet-isolated", nameof(Start_Dotnet_Isolated_InvalidHostJson_FailsWithExpectedError));
19+
await RunInvalidHostJsonTest("dotnet-isolated", nameof(Start_DotnetIsolated_InvalidHostJson_FailsWithExpectedError));
2120
}
2221

2322
[Fact(Skip = "Test fails and needs to be investiagted on why it does.")]
24-
public async Task Start_Dotnet_Isolated_MissingHostJson_FailsWithExpectedError()
23+
public async Task Start_DotnetIsolated_MissingHostJson_FailsWithExpectedError()
2524
{
26-
await RunMissingHostJsonTest("dotnet-isolated", nameof(Start_Dotnet_Isolated_MissingHostJson_FailsWithExpectedError));
25+
await RunMissingHostJsonTest("dotnet-isolated", nameof(Start_DotnetIsolated_MissingHostJson_FailsWithExpectedError));
2726
}
2827

2928
[Theory]
@@ -59,7 +58,7 @@ public async Task Start_LanguageWorker_InvalidFunctionJson_FailsWithExpectedErro
5958

6059
funcStartCommand.ProcessStartedHandler = async (process) =>
6160
{
62-
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter);
61+
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)));
6362
};
6463

6564
var result = funcStartCommand
@@ -100,7 +99,7 @@ public async Task Start_EmptyEnvVars_HandledAsExpected()
10099

101100
funcStartCommand.ProcessStartedHandler = async (process) =>
102101
{
103-
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter);
102+
await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)));
104103
};
105104

106105
var result = funcStartCommand

test/Cli/Func.E2E.Tests/Commands/FuncStart/MultipleFunctionsTests.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,18 @@ public async Task Start_FunctionsStartArgument_OnlySelectedFunctionsRun()
3333
{
3434
try
3535
{
36-
await ProcessHelper.WaitForFunctionHostToStart(process, port, funcStartCommand.FileWriter);
37-
36+
await ProcessHelper.WaitForFunctionHostToStart(process, port, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)));
37+
3838
using (var client = new HttpClient())
3939
{
4040
// http1 should be available
4141
var response1 = await client.GetAsync($"http://localhost:{port}/api/http1?name=Test");
4242
response1.StatusCode.Should().Be(HttpStatusCode.OK);
43+
4344
// http2 should be available
4445
var response2 = await client.GetAsync($"http://localhost:{port}/api/http2?name=Test");
4546
response2.StatusCode.Should().Be(HttpStatusCode.OK);
47+
4648
// http3 should not be available
4749
var response3 = await client.GetAsync($"http://localhost:{port}/api/http3?name=Test");
4850
response3.StatusCode.Should().Be(HttpStatusCode.NotFound);

0 commit comments

Comments
 (0)