From 759167303d425728c3d63de70e61fb819e1e84f7 Mon Sep 17 00:00:00 2001
From: Pavel Vostretsov
Date: Wed, 21 Feb 2024 12:41:15 +0500
Subject: [PATCH] try new versioning
---
.github/workflows/actions.yml | 41 ++--
CHANGELOG.md | 1 +
Directory.Build.props | 4 +-
Directory.Build.targets | 24 +--
NUnit.Middlewares/NUnit.Middlewares.csproj | 8 +
NUnit.Middlewares/README.md | 225 +++++++++++++++++++++
README.md | 223 +-------------------
version.json | 19 --
8 files changed, 271 insertions(+), 274 deletions(-)
create mode 100644 CHANGELOG.md
create mode 100644 NUnit.Middlewares/README.md
delete mode 100644 version.json
diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml
index 21234d3..0e58722 100644
--- a/.github/workflows/actions.yml
+++ b/.github/workflows/actions.yml
@@ -23,14 +23,14 @@ jobs:
run: dotnet build --configuration Release ./NUnit.Middlewares.sln
- name: Check codestyle
- run: dotnet jb cleanupcode NUnit.Middlewares.sln --profile=CatalogueCleanup --exclude=**/*.ts --verbosity=WARN && git diff --exit-code
+ run: dotnet jb cleanupcode NUnit.Middlewares.sln --profile=CatalogueCleanup --verbosity=WARN && git diff --exit-code
- name: Run tests
run: dotnet test --no-build --configuration Release ./NUnit.Middlewares.Tests/NUnit.Middlewares.Tests.csproj
publish:
runs-on: windows-2019
needs: test
- if: startsWith(github.event.ref, 'refs/tags/v')
+ if: github.ref_type == 'tag'
steps:
- uses: actions/checkout@v3
with:
@@ -41,28 +41,32 @@ jobs:
with:
global-json-file: global.json
- - name: Build
- run: dotnet build --configuration Release ./NUnit.Middlewares.sln
-
- name: Check version
run: |
$ErrorActionPreference = "Stop"
$tagName = "${{ github.ref_name }}"
- $version = $tagName.Substring(1)
- Write-Host "Will publish nuget package for $tagName tag" -ForegroundColor "Green"
- if ($tagName -match '^v\d+\.\d+-release') # tag name starts with 'vX.Y-release' (e.g. use 'v4.2-release.1' tag for the first patch for release v4.2)
- {
- $version = $version.Substring(0, $version.IndexOf("-release"))
- echo "SHOULD_CREATE_RELEASE=true" >> $env:GITHUB_ENV
- Write-Host "Will create release for $tagName tag" -ForegroundColor "Green"
- }
- $matchVersion = Select-String -Path ./version.json -Pattern "`"version`": `"$version`""
- if ($matchVersion -eq $null)
+
+ $regex = "^(?((\w+)\.)*\w+)\@(?(\d+\.\d+\.\d+)(?:-.+)?)$"
+ $match = [Regex]::Match($tagName, $regex).Groups
+ $packageName = $match["name"].Value
+ $version = $match["version"].Value
+ if ([string]::IsNullOrWhitespace($packageName) -or [string]::IsNullOrWhitespace($version))
{
- Write-Error "Version in tag ($version) does not match version in version.json"
+ Write-Error "Cannot parse invalid tag $tagName"
}
+
+ $pre = $version.Contains("-")
+ $release = if ($pre) { "prerelease" } else { "release" }
+
+ Write-Host "Will create $release for package $packageName ($version)" -ForegroundColor "Green"
+
+ echo "RELEASE_NOTE=https://github.com/skbkontur/nunit-middlewares/releases/tag/$tagName" >> $env:GITHUB_ENV
+ echo "PACKAGE_NAME=$packageName" >> $env:GITHUB_ENV
+ echo "VERSION=$version" >> $env:GITHUB_ENV
+ echo "PRE=$pre" >> $env:GITHUB_ENV
+
- name: Pack dotnet
- run: dotnet pack --no-build --configuration Release ./NUnit.Middlewares.sln
+ run: dotnet pack --configuration Release ./$env:PACKAGE_NAME/$env:PACKAGE_NAME.csproj -p:Version=$env:VERSION -p:PackageReleaseNotes=$env:RELEASE_NOTE
- name: Upload artifacts
uses: actions/upload-artifact@v3
@@ -77,9 +81,8 @@ jobs:
- name: Create release
uses: softprops/action-gh-release@v1
- if: ${{ env.SHOULD_CREATE_RELEASE == 'true' }}
with:
fail_on_unmatched_files: true
draft: false
- prerelease: false
+ prerelease: ${{ env.PRE == 'True' }}
files: "**/*.nupkg"
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5ddad42
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1 @@
+# Changelog
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
index 72846c0..f95d661 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -14,12 +14,14 @@
+ git
+ https://github.com/skbkontur/nunit-middlewares
+ $(RepositoryUrl)
true
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
-
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 4b16c02..27df599 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -1,22 +1,10 @@
-
-
-
-
-
-
-
-
-
- Pavel Vostretsov
- Middlewares for NUnit
- NUnit Middleware
- git
- https://github.com/skbkontur/nunit-middlewares
- $(RepositoryUrl)
- $(RepositoryUrl)/releases/tag/v$(MajorMinorVersion)-release
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/NUnit.Middlewares/NUnit.Middlewares.csproj b/NUnit.Middlewares/NUnit.Middlewares.csproj
index b2eccdc..7860ae7 100644
--- a/NUnit.Middlewares/NUnit.Middlewares.csproj
+++ b/NUnit.Middlewares/NUnit.Middlewares.csproj
@@ -5,10 +5,18 @@
SkbKontur.NUnit.Middlewares
SkbKontur.NUnit.Middlewares
SkbKontur.NUnit.Middlewares
+ Middlewares for NUnit
+ README.md
+ NUnit Middleware
+ Pavel Vostretsov
+
+
+
+
\ No newline at end of file
diff --git a/NUnit.Middlewares/README.md b/NUnit.Middlewares/README.md
new file mode 100644
index 0000000..8cf0e2a
--- /dev/null
+++ b/NUnit.Middlewares/README.md
@@ -0,0 +1,225 @@
+# NUnit.Middlewares
+
+[![NuGet Status](https://img.shields.io/nuget/v/SkbKontur.NUnit.Middlewares.svg)](https://www.nuget.org/packages/SkbKontur.NUnit.Middlewares/)
+[![Build status](https://github.com/skbkontur/nunit-middlewares/actions/workflows/actions.yml/badge.svg)](https://github.com/skbkontur/nunit-middlewares/actions)
+
+Use middleware pattern to write tests in concise and comprehensive manner. And ditch test bases.
+
+## Test setup middlewares
+
+Inspired by ASP.NET Core [middlewares](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware), the main idea of test middlewares can be summarized by this image:
+
+![nunit-middlewares](https://github.com/skbkontur/nunit-middlewares/assets/5417867/9707428f-11ec-4353-ac96-7fdf70200a47)
+
+Here we focus on *behaviours* that we want to add to our test rather than focusing on implementing test lifecycle methods provided by NUnit.
+
+`suite`, `fixture` and `test` in the image above are just `ISetupBuilder` that can accept either raw setup functions or anything that implements simple `ISetup` interface:
+
+![setup-builder](https://github.com/skbkontur/nunit-middlewares/assets/5417867/e4adb7c6-2078-401e-9bac-539f89ffec54)
+
+## Simple test base
+
+To inject this new behaviour into our tests, we will use two simple base classes: `SimpleSuiteBase` and `SimpleTestBase`, our tests from first image can be set up as follows:
+
+```csharp
+[SetUpFixture]
+public class PlaywrightSuite : SimpleSuiteBase
+{
+ protected override void Configure(ISetupBuilder suite)
+ {
+ suite
+ .UseHostingEnvironment()
+ .UseSimpleContainer()
+ .UseSetup();
+ }
+}
+
+public class BusinessObjectsSearchTests : SimpleTestBase
+{
+ [Injected] // Injected from container by `InitializeInjectedSetup`
+ private readonly IUserProvider userProvider;
+
+ protected override void Configure(ISetupBuilder fixture, ISetupBuilder test)
+ {
+ fixture
+ .UseSetup();
+
+ test
+ .UseSetup();
+ }
+
+ [Test]
+ public async Task BasicTest()
+ {
+ // every test gets its own browser, thus making tests easily parallelizable
+ var browser = SimpleTestContext.Current.Get();
+
+ await browser.LoginAsync(userProvider.DefaultUser);
+ await browser.Page.GotoAsync("https://google.com");
+ await browser.Page.GetByTitle("Search").FillAsync("nunit");
+ }
+}
+```
+
+## Composition over inheritance
+
+With the power of C#'s extension methods, we can use composition of setups instead of relying on inheritance. For example, here's how setup for our container can be written:
+
+```csharp
+public static class SetupExtensions
+{
+ public static ISetupBuilder UseSimpleContainer(
+ this ISetupBuilder builder,
+ Action? configure = null)
+ {
+ return builder
+ // our container needs hosting environment, hence we should always set it up,
+ // but if it was already set up earlier, we will use existing environment
+ .UseSetup(new HostingEnvironmentSetup(setupOnlyIfNotExists: true))
+ .UseSetup(new SimpleContainerSetup(configure));
+ }
+}
+
+public class SimpleContainerSetup : ISetup
+{
+ private readonly Action? configure;
+
+ public SimpleContainerSetup(Action? configure)
+ {
+ this.configure = configure;
+ }
+
+ public Task SetUpAsync(ITest test)
+ {
+ var environment = test.GetFromThisOrParentContext();
+ var container = ContainerFactory.NewContainer(environment, configure);
+ test.Properties.Set(container); // save container to current test context
+
+ return Task.CompletedTask;
+ }
+
+ public Task TearDownAsync(ITest test)
+ {
+ var container = test.Properties.Get();
+ container.Dispose();
+
+ return Task.CompletedTask;
+ }
+}
+```
+
+Using these building blocks, we can move all the complexity of setups to separate, smaller code pieces (`ISetup`s), and make setups more reusable in the process.
+
+## Simple test context
+
+In our `BasicTest` above we used `SimpleTestContext.Current.Get()` to get browser that we set up in `BrowserPerTestSetup`. Also, in `SimpleContainerSetup` we used `GetFromThisOrParentContext` method that can access items that previous setups have set up. How does it work? Good news is that we can use built-in NUnit features to build such test context.
+
+`TestExecutionContext.CurrentContext.CurrentTest` - current test, implements `ITest`
+
+How do we get container/browser from suite context in our test? Every test has property `IPropertyBag Properties`.
+
+Tests in NUnit are represented by a tree-like structure, and `ITest` has access to parent through `ITest Parent` property. Parent for test method is test fixture, parent for fixture is suite and so on.
+
+That means we can search for *context item* of interest in parent, if not found - in parent's parent
+
+To ensure everything is working as intended, parent's *context item*s should be used as **readonly**
+
+In our example from first image, test context will look something like this:
+
+![test-context](https://github.com/skbkontur/nunit-middlewares/assets/5417867/c70b41d6-5f3f-485a-9e9d-7616b3797232)
+
+Both `SimpleTestContext` and `GetFromThisOrParentContext` are just `ITest` wrappers that search for context value in `ITest`'s `Properties` recursively
+
+## Why are test bases a problem?
+
+To make a point, let's try to rewrite test above without our testing machinery.
+
+Let's start with `BusinessObjectsSearchTests.cs`:
+
+```csharp
+public class BusinessObjectsSearchTests : PlaywrightTestBase
+{
+ [Injected]
+ private readonly IUserProvider userProvider;
+
+ [Test]
+ public async Task BasicTest()
+ {
+ // every test gets its own browser, thus making tests easily parallelizable
+ await using var browser = await BrowserPerTest();
+
+ await browser.LoginAsync(userProvider.DefaultUser);
+ await browser.Page.GotoAsync("https://google.com");
+ await browser.Page.GetByTitle("Search").FillAsync("nunit");
+ }
+}
+```
+
+So far so good, notice that we moved `BrowserPerTestSetup` into the test itself. A neat trick that would be more difficult if we had more per test instances to set up.
+
+`PlaywrightTestBase` looks simple enough. But we had to make our Browser `IAsyncDisposable`:
+
+```csharp
+public class PlaywrightTestBase : SimpleContainerTestBase
+{
+ protected IPlaywright playwright;
+ protected IBrowser browser;
+
+ [OneTimeSetUp]
+ public async Task SetUpPlaywright()
+ {
+ playwright = await Playwright.CreateAsync();
+ browser = await playwright.Chromium.LaunchAsync()
+ }
+
+ [OneTimeTearDown]
+ public async Task TearDownPlaywright()
+ {
+ await browser.DisposeAsync().ConfigureAwait(false);
+ playwright.Dispose();
+ }
+
+ protected async Task BrowserPerTest()
+ {
+ var page = await browser.NewPageAsync();
+ return new Browser(page); // now Browser is responsible for disposing of page
+ }
+}
+```
+
+How deep does this rabbit hole go? Let's dive into `SimpleContainerTestBase`:
+
+```csharp
+public class SimpleContainerTestBase
+{
+ protected IContainer container;
+
+ [OneTimeSetUp]
+ public void SetUpContainer()
+ {
+ var environment = HostingEnvironment.Create();
+ container = ContainerFactory.NewContainer(environment, ConfigureContainer);
+ ContainerFactory.InitializeInjectedFields(container, this);
+ }
+
+ [OneTimeTearDown]
+ public void TearDownContainer()
+ {
+ container.Dispose();
+ }
+
+ protected virtual void ConfigureContainer(ContainerBuilder builder)
+ {
+ }
+}
+```
+
+Now it doesn't look that bad. What did we miss? Quite a few things:
+- it was harder to setup items per test and keep tests parallelizable
+- to shorten chain of inheritance, we tightly integrated setup of HostingEnvironment and Container and forgot to dispose of hosting environment
+- we set up container and hosting environment for each test, before we only set it up once. Refactoring it can be a PITA, especially if `container` or `browser` field is referenced in our tests. On the other hand, when using nunit-middlewares, we can refactor such case by moving two lines of code.
+- what if many of our test fixtures need an organization to work with? would we make `class OrganizationTestBase : PlaywrightTestBase`? and if we need an organization, but don't need browser?
+- our example is rather simple, in more complex cases, our test bases can quickly become a nightmare to debug and extend
+
+Excellent example of a complex case is playwright integration with nunit in official [Playwright.NUnit](https://github.com/microsoft/playwright-dotnet/tree/main/src/Playwright.NUnit) package:
+- it has `PageTest` that inherits `ContextTest` that inherits `BrowserTest` that inherits `PlaywrightTest` that inherits `WorkerAwareTest`... whoa
\ No newline at end of file
diff --git a/README.md b/README.md
index 1324b3c..71fa646 100644
--- a/README.md
+++ b/README.md
@@ -1,227 +1,16 @@
-# nunit-middlewares
+# nunit-extensions
-Use middleware pattern to write tests in concise and comprehensive manner. And ditch test bases.
+A collection of extensions for NUnit.
| | Build Status |
|-------------------------------------|:--------------: |
| NUnit.Middlewares | [![NuGet Status](https://img.shields.io/nuget/v/SkbKontur.NUnit.Middlewares.svg)](https://www.nuget.org/packages/SkbKontur.NUnit.Middlewares/) |
| Build | [![Build status](https://github.com/skbkontur/nunit-middlewares/actions/workflows/actions.yml/badge.svg)](https://github.com/skbkontur/nunit-middlewares/actions) |
-## Test setup middlewares
+## Release Notes
-Inspired by ASP.NET Core [middlewares](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware), the main idea of test middlewares can be summarized by this image:
+See [CHANGELOG](CHANGELOG.md).
-![nunit-middlewares](https://github.com/skbkontur/nunit-middlewares/assets/5417867/9707428f-11ec-4353-ac96-7fdf70200a47)
+## Projects
-Here we focus on *behaviours* that we want to add to our test rather than focusing on implementing test lifecycle methods provided by NUnit.
-
-`suite`, `fixture` and `test` in the image above are just `ISetupBuilder` that can accept either raw setup functions or anything that implements simple `ISetup` interface:
-
-![setup-builder](https://github.com/skbkontur/nunit-middlewares/assets/5417867/e4adb7c6-2078-401e-9bac-539f89ffec54)
-
-## Simple test base
-
-To inject this new behaviour into our tests, we will use two simple base classes: `SimpleSuiteBase` and `SimpleTestBase`, our tests from first image can be set up as follows:
-
-```csharp
-[SetUpFixture]
-public class PlaywrightSuite : SimpleSuiteBase
-{
- protected override void Configure(ISetupBuilder suite)
- {
- suite
- .UseHostingEnvironment()
- .UseSimpleContainer()
- .UseSetup();
- }
-}
-
-public class BusinessObjectsSearchTests : SimpleTestBase
-{
- [Injected] // Injected from container by `InitializeInjectedSetup`
- private readonly IUserProvider userProvider;
-
- protected override void Configure(ISetupBuilder fixture, ISetupBuilder test)
- {
- fixture
- .UseSetup();
-
- test
- .UseSetup();
- }
-
- [Test]
- public async Task BasicTest()
- {
- // every test gets its own browser, thus making tests easily parallelizable
- var browser = SimpleTestContext.Current.Get();
-
- await browser.LoginAsync(userProvider.DefaultUser);
- await browser.Page.GotoAsync("https://google.com");
- await browser.Page.GetByTitle("Search").FillAsync("nunit");
- }
-}
-```
-
-## Composition over inheritance
-
-With the power of C#'s extension methods, we can use composition of setups instead of relying on inheritance. For example, here's how setup for our container can be written:
-
-```csharp
-public static class SetupExtensions
-{
- public static ISetupBuilder UseSimpleContainer(
- this ISetupBuilder builder,
- Action? configure = null)
- {
- return builder
- // our container needs hosting environment, hence we should always set it up,
- // but if it was already set up earlier, we will use existing environment
- .UseSetup(new HostingEnvironmentSetup(setupOnlyIfNotExists: true))
- .UseSetup(new SimpleContainerSetup(configure));
- }
-}
-
-public class SimpleContainerSetup : ISetup
-{
- private readonly Action? configure;
-
- public SimpleContainerSetup(Action? configure)
- {
- this.configure = configure;
- }
-
- public Task SetUpAsync(ITest test)
- {
- var environment = test.GetFromThisOrParentContext();
- var container = ContainerFactory.NewContainer(environment, configure);
- test.Properties.Set(container); // save container to current test context
-
- return Task.CompletedTask;
- }
-
- public Task TearDownAsync(ITest test)
- {
- var container = test.Properties.Get();
- container.Dispose();
-
- return Task.CompletedTask;
- }
-}
-```
-
-Using these building blocks, we can move all the complexity of setups to separate, smaller code pieces (`ISetup`s), and make setups more reusable in the process.
-
-## Simple test context
-
-In our `BasicTest` above we used `SimpleTestContext.Current.Get()` to get browser that we set up in `BrowserPerTestSetup`. Also, in `SimpleContainerSetup` we used `GetFromThisOrParentContext` method that can access items that previous setups have set up. How does it work? Good news is that we can use built-in NUnit features to build such test context.
-
-`TestExecutionContext.CurrentContext.CurrentTest` - current test, implements `ITest`
-
-How do we get container/browser from suite context in our test? Every test has property `IPropertyBag Properties`.
-
-Tests in NUnit are represented by a tree-like structure, and `ITest` has access to parent through `ITest Parent` property. Parent for test method is test fixture, parent for fixture is suite and so on.
-
-That means we can search for *context item* of interest in parent, if not found - in parent's parent
-
-To ensure everything is working as intended, parent's *context item*s should be used as **readonly**
-
-In our example from first image, test context will look something like this:
-
-![test-context](https://github.com/skbkontur/nunit-middlewares/assets/5417867/c70b41d6-5f3f-485a-9e9d-7616b3797232)
-
-Both `SimpleTestContext` and `GetFromThisOrParentContext` are just `ITest` wrappers that search for context value in `ITest`'s `Properties` recursively
-
-## Why are test bases a problem?
-
-To make a point, let's try to rewrite test above without our testing machinery.
-
-Let's start with `BusinessObjectsSearchTests.cs`:
-
-```csharp
-public class BusinessObjectsSearchTests : PlaywrightTestBase
-{
- [Injected]
- private readonly IUserProvider userProvider;
-
- [Test]
- public async Task BasicTest()
- {
- // every test gets its own browser, thus making tests easily parallelizable
- await using var browser = await BrowserPerTest();
-
- await browser.LoginAsync(userProvider.DefaultUser);
- await browser.Page.GotoAsync("https://google.com");
- await browser.Page.GetByTitle("Search").FillAsync("nunit");
- }
-}
-```
-
-So far so good, notice that we moved `BrowserPerTestSetup` into the test itself. A neat trick that would be more difficult if we had more per test instances to set up.
-
-`PlaywrightTestBase` looks simple enough. But we had to make our Browser `IAsyncDisposable`:
-
-```csharp
-public class PlaywrightTestBase : SimpleContainerTestBase
-{
- protected IPlaywright playwright;
- protected IBrowser browser;
-
- [OneTimeSetUp]
- public async Task SetUpPlaywright()
- {
- playwright = await Playwright.CreateAsync();
- browser = await playwright.Chromium.LaunchAsync()
- }
-
- [OneTimeTearDown]
- public async Task TearDownPlaywright()
- {
- await browser.DisposeAsync().ConfigureAwait(false);
- playwright.Dispose();
- }
-
- protected async Task BrowserPerTest()
- {
- var page = await browser.NewPageAsync();
- return new Browser(page); // now Browser is responsible for disposing of page
- }
-}
-```
-
-How deep does this rabbit hole go? Let's dive into `SimpleContainerTestBase`:
-
-```csharp
-public class SimpleContainerTestBase
-{
- protected IContainer container;
-
- [OneTimeSetUp]
- public void SetUpContainer()
- {
- var environment = HostingEnvironment.Create();
- container = ContainerFactory.NewContainer(environment, ConfigureContainer);
- ContainerFactory.InitializeInjectedFields(container, this);
- }
-
- [OneTimeTearDown]
- public void TearDownContainer()
- {
- container.Dispose();
- }
-
- protected virtual void ConfigureContainer(ContainerBuilder builder)
- {
- }
-}
-```
-
-Now it doesn't look that bad. What did we miss? Quite a few things:
-- it was harder to setup items per test and keep tests parallelizable
-- to shorten chain of inheritance, we tightly integrated setup of HostingEnvironment and Container and forgot to dispose of hosting environment
-- we set up container and hosting environment for each test, before we only set it up once. Refactoring it can be a PITA, especially if `container` or `browser` field is referenced in our tests. On the other hand, when using nunit-middlewares, we can refactor such case by moving two lines of code.
-- what if many of our test fixtures need an organization to work with? would we make `class OrganizationTestBase : PlaywrightTestBase`? and if we need an organization, but don't need browser?
-- our example is rather simple, in more complex cases, our test bases can quickly become a nightmare to debug and extend
-
-Excellent example of a complex case is playwright integration with nunit in official [Playwright.NUnit](https://github.com/microsoft/playwright-dotnet/tree/main/src/Playwright.NUnit) package:
-- it has `PageTest` that inherits `ContextTest` that inherits `BrowserTest` that inherits `PlaywrightTest` that inherits `WorkerAwareTest`... whoa
\ No newline at end of file
+- [NUnit.Middlewares](NUnit.Middlewares)
\ No newline at end of file
diff --git a/version.json b/version.json
deleted file mode 100644
index 38f6efb..0000000
--- a/version.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "version": "0.1",
- "assemblyVersion": {
- "precision": "build"
- },
- "publicReleaseRefSpec": [
- "^refs/heads/master$",
- "^refs/tags/v\\d+\\.\\d+"
- ],
- "nugetPackageVersion": {
- "semVer": 2
- },
- "cloudBuild": {
- "setVersionVariables": true,
- "buildNumber": {
- "enabled": false
- }
- }
-}
\ No newline at end of file