diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9831879 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +[*.cs] + +csharp_style_expression_bodied_methods=when_on_single_line +dotnet_analyzer_diagnostic.severity = error +dotnet_diagnostic.CS1591.severity = none +# dotnet_diagnostic.CA1051.severity = none +# dotnet_diagnostic.CA1304.severity = none +# dotnet_diagnostic.CA1305.severity = none +# dotnet_diagnostic.CA1309.severity = none +# dotnet_diagnostic.CA1310.severity = none +# dotnet_diagnostic.CA1715.severity = none +dotnet_diagnostic.CA1822.severity = none +dotnet_diagnostic.IDE0008.severity = none +dotnet_diagnostic.IDE0039.severity = none +dotnet_diagnostic.IDE0052.severity = none +dotnet_diagnostic.IDE0058.severity = none +dotnet_diagnostic.IDE0063.severity = none +dotnet_diagnostic.IDE0065.severity = none +dotnet_diagnostic.IDE0090.severity = none +dotnet_diagnostic.JSON002.severity = none diff --git a/.github/fix-docs-version.sh b/.github/fix-docs-version.sh new file mode 100755 index 0000000..4cc76c9 --- /dev/null +++ b/.github/fix-docs-version.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# This script takes care of setting proper JMeter DSL version in docs. +# +set -eo pipefail + +VERSION=$1 + +update_file_versions() { + local VERSION="$1" + local FILE="$2" + sed -i "s/--version [0-9.]\+/--version ${VERSION}/g" "${FILE}" +} + +update_file_versions ${VERSION} README.md + +find docs -name "*.md" -not -path "*/node_modules/*" | while read DOC_FILE; do + update_file_versions ${VERSION} ${DOC_FILE} +done \ No newline at end of file diff --git a/.github/fix-project-version.sh b/.github/fix-project-version.sh new file mode 100755 index 0000000..91059ec --- /dev/null +++ b/.github/fix-project-version.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# This script takes care of setting proper JMeter DSL version in projects. +# +set -eo pipefail + +VERSION=$1 + +update_file_versions() { + local VERSION="$1" + local FILE="$2" + sed -i "s/[0-9.]\+<\/AssemblyVersion>/${VERSION}.0<\/AssemblyVersion>/g" "${FILE}" + sed -i "s/[0-9.]\+<\/Version>/${VERSION}<\/Version>/g" "${FILE}" + sed -i "s/[0-9.]\+<\/FileVersion>/${VERSION}<\/FileVersion>/g" "${FILE}" +} + +find . -name "*.csproj" | while read DOC_FILE; do + update_file_versions ${VERSION} ${DOC_FILE} +done \ No newline at end of file diff --git a/.github/next-minor-alpha.sh b/.github/next-minor-alpha.sh new file mode 100755 index 0000000..4887435 --- /dev/null +++ b/.github/next-minor-alpha.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# +# This script solves next minor version +set -eo pipefail + +VERSION="$1" +MAJOR="${VERSION%%.*}" +VERSION="${VERSION#*.}" +MINOR="${VERSION%%.*}" +echo "${MAJOR}.$((MINOR + 1))-alpha1" diff --git a/.github/nuget-deploy.sh b/.github/nuget-deploy.sh new file mode 100755 index 0000000..24d6830 --- /dev/null +++ b/.github/nuget-deploy.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# +# This script takes care of deploying packages to nuget +# +# Required environment variables: NUGET_API_KEY + +set -eo pipefail + +push_package() { + dotnet nuget push "$1" --api-key "${NUGET_API_KEY}" --source https://api.nuget.org/v3/index.json +} + +find . -name "*.nupkg" -or -name "*.snupkg" | while read PACKAGE; do + push_package ${PACKAGE} +done diff --git a/.github/semver-check.sh b/.github/semver-check.sh new file mode 100755 index 0000000..2852215 --- /dev/null +++ b/.github/semver-check.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# This script checks that the version number of the release is an expected one, and avoid erroneous releases which don't follow semver +set -eo pipefail + +git fetch --tags --quiet +VERSION="$1" +PREV_VERSION=$(git tag --sort=-creatordate | head -1) +PREV_VERSION=${PREV_VERSION:-v0.0} +PREV_VERSION=${PREV_VERSION#v} +PREV_MAJOR="${PREV_VERSION%%.*}" +PREV_VERSION="${PREV_VERSION#*.}" +PREV_MINOR="${PREV_VERSION%%.*}" +PREV_PATCH="${PREV_VERSION#*.}" +if [[ "$PREV_VERSION" == "$PREV_PATCH" ]]; then + PREV_PATCH="0" +fi + +[[ "$VERSION" == "$PREV_MAJOR.$PREV_MINOR.$((PREV_PATCH + 1))" || "$VERSION" == "$PREV_MAJOR.$((PREV_MINOR + 1))" || "$VERSION" == "$((PREV_MAJOR + 1)).0" ]] diff --git a/.github/update-sample-version.sh b/.github/update-sample-version.sh new file mode 100755 index 0000000..788b384 --- /dev/null +++ b/.github/update-sample-version.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# This script takes care of updating JMeter DSL version in sample project. +# +set -eox pipefail + +VERSION=$1 + +USER_EMAIL="$(git log --format='%ae' HEAD^!)" +USER_NAME="$(git log --format='%an' HEAD^!)" + +cd jmeter-dotnet-dsl-sample +sed -i 's/Include="Abstracta.JmeterDsl" Version="[0-9.]\+"/Include="Abstracta.JmeterDsl" Version="${VERSION}"/g' Abstracta.JmeterDsl.Sample/Abstracta.JmeterDsl.Sample.csproj + +git add . +git config --local user.email "$USER_EMAIL" +git config --local user.name "$USER_NAME" +git commit -m "Updated Abstracta.JmeterDsl version" +git push origin HEAD:master +cd .. +rm -rf jmeter-dotnet-dsl-sample diff --git a/.github/vuepress-deploy.sh b/.github/vuepress-deploy.sh new file mode 100755 index 0000000..a029a1b --- /dev/null +++ b/.github/vuepress-deploy.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env sh +# +# This script takes care of building and deploying the vuepress documentation to github pages. +# +set -e + +cd docs + +pnpm install && pnpm build + +cd .vuepress/dist + +EMAIL="$(git log --format='%ae' HEAD^!)" +USERNAME="$(git log --format='%an' HEAD^!)" +git init +git config --local user.email "$EMAIL" +git config --local user.name "$USERNAME" +git add . +git commit -m '[skip ci] Deploy docs to GitHub pages' + +git push -f https://git:${ACCESS_TOKEN}@github.com/abstracta/jmeter-java-dsl.git master:gh-pages + +cd $GITHUB_WORKSPACE diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..de83150 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,32 @@ +name: run tests +on: + push: + tags-ignore: + - "*" + branches: + - "**" +jobs: + test: + runs-on: ubuntu-latest + concurrency: azure_test + steps: + - uses: actions/checkout@v3 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6 + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 8 + cache: maven + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore + - name: Test + # avoid running tests in parallel since it may happen that two tests try to modify .jmeter-dsl directory + run: dotnet test -m:1 --no-build --configuration Release --verbosity normal + env: + AZURE_CREDS: ${{ secrets.AZURE_CREDS }} + BZ_TOKEN: ${{ secrets.BZ_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d407a07 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: release +run-name: release ${{ inputs.version }} +on: + workflow_dispatch: + inputs: + version: + required: true + type: string +jobs: + release: + runs-on: ubuntu-latest + concurrency: remote_test + steps: + - uses: actions/checkout@v3 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6 + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 8 + cache: maven + - uses: actions/setup-node@v3 + with: + node-version: '18' + - name: check version + run: .github/semver-check.sh ${{ inputs.version }} + - name: create release draft + uses: ncipollo/release-action@v1 + with: + tag: v${{ inputs.version }} + name: ${{ inputs.version }} + draft: true + - name: set project version + run: .github/fix-project-version.sh ${{ inputs.version }} + - name: update docs version + run: .github/fix-docs-version.sh ${{ inputs.version }} + - name: commit release version + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: '[skip ci] Set release version' + branch: master + file_pattern: '*/*.csproj README.md docs/index.md docs/guide/**' + - name: Build + run: dotnet build --configuration Release + - name: Test + # avoid running tests in parallel since it may happen that two tests try to modify .jmeter-dsl directory + run: dotnet test -m:1 --no-build --configuration Release --verbosity normal + env: + AZURE_CREDS: ${{ secrets.AZURE_CREDS }} + BZ_TOKEN: ${{ secrets.BZ_TOKEN }} + - name: Package + run: dotnet pack --no-build --configuration Release + - name: publish to Nuget + run: .github/nuget-deploy.sh + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + - name: publish GH release + uses: ncipollo/release-action@v1 + with: + tag: v${{ inputs.version }} + allowUpdates: true + omitNameDuringUpdate: true + omitBodyDuringUpdate: true + updateOnlyUnreleased: true + draft: false + - name: get next alpha version + run: echo "ALPHA_VERSION=$(.github/next-minor-alpha.sh ${{ inputs.version }})" >> $GITHUB_ENV + - name: update to next ALPHA version + run: .github/fix-project-version.sh ${{ env.ALPHA_VERSION }} + - name: commit ALPHA version + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: '[skip ci] Set ALPHA version' + branch: master + file_pattern: '*/*.csproj README.md docs/index.md docs/guide/**' + - name: deploy github pages + run: .github/vuepress-deploy.sh + env: + ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v3 + with: + repository: abstracta/jmeter-dotnet-dsl-sample + path: jmeter-dotnet-dsl-sample + token: ${{ secrets.ACTIONS_TOKEN }} + - name: update version in sample project + run: .github/update-sample-version.sh ${{ inputs.version }} + - name: Discord notification + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + uses: Ilshidur/action-discord@master + with: + args: 'A new release is out! Check it at https://github.com/abstracta/jmeter-dotnet-dsl/releases/tag/v${{ inputs.version }}' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d49c563 --- /dev/null +++ b/.gitignore @@ -0,0 +1,413 @@ +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# content below from: https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +#### CUSTOM + +.vscode +node_modules +.temp/ +.cache/ +dist/ diff --git a/Abstracta.JmeterDsl.Azure.Tests/Abstracta.JmeterDsl.Azure.Tests.csproj b/Abstracta.JmeterDsl.Azure.Tests/Abstracta.JmeterDsl.Azure.Tests.csproj new file mode 100644 index 0000000..3e22ce2 --- /dev/null +++ b/Abstracta.JmeterDsl.Azure.Tests/Abstracta.JmeterDsl.Azure.Tests.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + false + true + + + + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/Abstracta.JmeterDsl.Azure.Tests/AzureEngineTest.cs b/Abstracta.JmeterDsl.Azure.Tests/AzureEngineTest.cs new file mode 100644 index 0000000..99c6d31 --- /dev/null +++ b/Abstracta.JmeterDsl.Azure.Tests/AzureEngineTest.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; + +namespace Abstracta.JmeterDsl.Azure.Tests +{ + using static JmeterDsl; + + public class AzureEngineTest + { + + private TextWriter originalConsoleOut; + + // Redirecting output to progress to get live stdout with nunit. + // https://github.com/nunit/nunit3-vs-adapter/issues/343 + // https://github.com/nunit/nunit/issues/1139 + [SetUp] + public void SetUp() + { + originalConsoleOut = Console.Out; + Console.SetOut(TestContext.Progress); + } + + [TearDown] + public void TearDown() => + Console.SetOut(originalConsoleOut!); + + [Test] + public void TestInAzure() + { + var stats = TestPlan( + ThreadGroup(1, 1, + HttpSampler("http://localhost") + ) + ).RunIn(new AzureEngine(Environment.GetEnvironmentVariable("AZURE_CREDS"))); + Assert.That(stats.Overall.ErrorsCount, Is.EqualTo(1)); + } + } +} diff --git a/Abstracta.JmeterDsl.Azure.Tests/Usings.cs b/Abstracta.JmeterDsl.Azure.Tests/Usings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/Abstracta.JmeterDsl.Azure.Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/Abstracta.JmeterDsl.Azure.Tests/log4j2.xml b/Abstracta.JmeterDsl.Azure.Tests/log4j2.xml new file mode 100644 index 0000000..e04775a --- /dev/null +++ b/Abstracta.JmeterDsl.Azure.Tests/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Abstracta.JmeterDsl.Azure/Abstracta.JmeterDsl.Azure.csproj b/Abstracta.JmeterDsl.Azure/Abstracta.JmeterDsl.Azure.csproj new file mode 100644 index 0000000..e221fc4 --- /dev/null +++ b/Abstracta.JmeterDsl.Azure/Abstracta.JmeterDsl.Azure.csproj @@ -0,0 +1,48 @@ + + + + netstandard2.0 + Abstracta.JmeterDsl.Azure + ../StyleCop.ruleset + true + true + Abstracta.JmeterDsl.Azure + Abstracta.JmeterDsl.Azure + Abstracta + Abstracta + Module which allows to easily run Abstracta.JmeterDsl test plans at scale in Azure Load Testing. + true + jmeter,performance,load,test + logo.png + Apache-2.0 + https://abstracta.github.io/jmeter-dotnet-dsl + README.md + true + snupkg + true + 0.1.0.0 + 0.1 + 0.1 + + + true + + + + + + + + + + + + + + + + + + + + diff --git a/Abstracta.JmeterDsl.Azure/AzureEngine.cs b/Abstracta.JmeterDsl.Azure/AzureEngine.cs new file mode 100644 index 0000000..b20ff4e --- /dev/null +++ b/Abstracta.JmeterDsl.Azure/AzureEngine.cs @@ -0,0 +1,193 @@ +using System; +using Abstracta.JmeterDsl.Core.Engines; + +namespace Abstracta.JmeterDsl.Azure +{ + /// + /// A which allows running DslTestPlan in Azure Load Testing. + ///
+ /// To use this engine you need: + ///
    + ///
  • To create a test resource and resource group in Azure Load Testing.First defined test + /// resource for the subscription, is used by default.
  • + ///
  • Register an application in Azure with proper permissions for Azure Load Testing with an + /// associated secret. Check here + /// for more details.
  • + ///
+ ///
+ public class AzureEngine : BaseJmeterEngine + { + private readonly string _credentials; + private string _subscriptionId; + private string _resourceGroupName; + private string _location; + private string _testResourceName; + private string _testName = "jmeter-dotnet-dsl"; + private TimeSpan? _testTimeout; + private int? _engines; + + /// + /// Builds a new AzureEngine from a given string containing tenant id, client id and client secrets + /// separated by colons. + ///
+ /// This is just a handy way to specify credentials in a string (eg: environment variable) and + /// easily create an Azure Engine. For a more explicit way you may use . + ///
+ /// contains tenant id, client id and client secrets separated by colons. Eg: + /// myTenantId:myClientId:mySecret. + ///
+ /// Check the + /// Azure guide for instructions on how to register an application with + /// proper access. Tip: there is no need to specify a redirect uri. + ///
+ /// The tenantId can easily be retrieved getting subscription info in Azure Portal. + /// + public AzureEngine(string credentials) + { + _credentials = credentials; + } + + /// + /// This is a more explicit way to create AzureEngine than . + ///
+ /// This is usually preferred when you already have each credential value separated, as is more + /// explicit and doesn't require encoding into a string. + ///
+ /// is the tenant id for your subscription. This can easily be retrieved + /// getting subscription info in Azure Portal + /// this is the id associated to the test that needs to run in Azure. You + /// should use one application for each JMeter DSL project that uses Azure Load + /// Testing. This can be retrieved when register an application following steps + /// detailed in this Azure guide. + /// + /// this is a client secret generated for the test to be run in Azure. + public AzureEngine(string tenantId, string clientId, string clientSecret) + { + _credentials = $"{tenantId}:{clientId}:{clientSecret}"; + } + + /// + /// Allows specifying the Azure subscription ID to run the tests on. + ///
+ /// By default, AzureEngine will use any subscription associated to the given tenant. In most of + /// the scenarios, when you only use one subscription, this behavior is good. But, when you have + /// multiple subscriptions, it is necessary to specify which subscription from the available ones + /// you want to use. This method is for those scenarios. + ///
+ /// specifies the Azure subscription identifier to use while running tests. + /// When not specified, any subscription associated to the tenant will be + /// used. + /// the engine for further configuration or usage. + public AzureEngine SubscriptionId(string subscriptionId) + { + _subscriptionId = subscriptionId; + return this; + } + + /// + /// Specifies the name the resource group where tests will be created or updated. + ///
+ /// You can use Azure resource groups to group different test resources (projects, systems under + /// test) shared by members of a team. + ///
+ /// If a resource group exists with the given name, then that group will be used. Otherwise, a new + /// one will be created. You can use to specify the location where the + /// resource group will be created (by default, the first available one will be used, eg: eastus). + ///
+ /// specifies the name of the resource group to use. If no name is + /// specified, then the test resource name () + /// plus "-rg" suffix is used. Eg: jmeter-dotnet-dsl-rg. + /// the engine for further configuration or usage. + public AzureEngine ResourceGroupName(string resourceGroupName) + { + _resourceGroupName = resourceGroupName; + return this; + } + + /// + /// Specifies the location where to create new resource groups. + /// + /// the Azure location to use when creating new resource groups. If none is + /// specified, then the first available location will be used (eg: eastus). + /// the engine for further configuration or usage. + /// + public AzureEngine Location(string location) + { + _location = location; + return this; + } + + /// + /// Specifies the name of the test resource where tests will be created or updated. + ///
+ /// You can use Azure test resources to group different tests resources belonging to the same + /// project or system under test. + ///
+ /// If a test resource exists with the given name, then that test resources will be used. + /// Otherwise, a new one will be created. + ///
+ /// specifies the name of the test resource. If no name is specified, then + /// the test name () is used. + /// the engine for further configuration or usage. + public AzureEngine TestResourceName(string testResourceName) + { + _testResourceName = testResourceName; + return this; + } + + /// + /// Specifies the name of the test to be created or updated. + ///
+ /// If a test with the given name exists, then the test is updated. Otherwise, a new one is + /// created. + ///
+ /// specifies the name of the test to create or update. If no name is specified, + /// then jmeter-dotnet-dsl is used by default. + /// the engine for further configuration or usage. + public AzureEngine TestName(string testName) + { + _testName = testName; + return this; + } + + /// + /// Specifies a timeout for the entire test execution. + ///
+ /// If the timeout is reached then the test run will throw a JvmException. + ///
+ /// It is strongly advised to set this timeout properly in each run, according to the expected test + /// execution time plus some additional margin (to consider for additional delays in BlazeMeter + /// test setup and teardown). + ///
+ /// This timeout exists to avoid any potential problem with BlazeMeter execution not detected by + /// the client, and avoid keeping the test indefinitely running until is interrupted by a user. + /// This is specially annoying when running tests in automated fashion, for example in CI/CD. + ///
+ /// When not specified, the default timeout will is set to 1 hour. + ///
+ /// to be used as time limit for test execution. If execution takes more than this, + /// then a JvmException will be thrown by the engine. + /// the engine for further configuration or usage. + public AzureEngine TestTimeout(TimeSpan duration) + { + _testTimeout = duration; + return this; + } + + /// + /// Specifies the number of JMeter engine instances where the test plan should run. + ///
+ /// This value directly impact the generated load. For example: if your test plan defines to use a + /// thread group with 100 users, then using 3 engines will result in 300 parallel users. Azure Load + /// Testing simply runs the test plan in as many engines specified by this value. + ///
+ /// specifies the number of JMeter engine instances to run the test plan on. When not + /// specified it just runs the test plan in 1 engine. + /// the engine for further configuration or usage. + public AzureEngine Engines(int count) + { + _engines = count; + return this; + } + } +} diff --git a/Abstracta.JmeterDsl.Azure/pom.xml b/Abstracta.JmeterDsl.Azure/pom.xml new file mode 100644 index 0000000..84f89d2 --- /dev/null +++ b/Abstracta.JmeterDsl.Azure/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + us.abstracta.jmeter.dotnet + jmeter-dotnet-dsl-parent + 1.0-SNAPSHOT + ../pom.xml + + jmeter-dotnet-dsl-azure + pom + + This pom is only needed to be able to copy jmeter-java-dsl-azure jar and its dependencies with dependency:copy-dependencies maven plugin goal + + + + us.abstracta.jmeter + jmeter-java-dsl-azure + ${jmeter-java-dsl.version} + + + us.abstracta.jmeter + jmeter-java-dsl + + + + + \ No newline at end of file diff --git a/Abstracta.JmeterDsl.BlazeMeter.Tests/Abstracta.JmeterDsl.BlazeMeter.Tests.csproj b/Abstracta.JmeterDsl.BlazeMeter.Tests/Abstracta.JmeterDsl.BlazeMeter.Tests.csproj new file mode 100644 index 0000000..e192756 --- /dev/null +++ b/Abstracta.JmeterDsl.BlazeMeter.Tests/Abstracta.JmeterDsl.BlazeMeter.Tests.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + false + true + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Abstracta.JmeterDsl.BlazeMeter.Tests/BlazeMeterEngineTests.cs b/Abstracta.JmeterDsl.BlazeMeter.Tests/BlazeMeterEngineTests.cs new file mode 100644 index 0000000..21a4dc7 --- /dev/null +++ b/Abstracta.JmeterDsl.BlazeMeter.Tests/BlazeMeterEngineTests.cs @@ -0,0 +1,37 @@ +using System; +using System.IO; + +namespace Abstracta.JmeterDsl.BlazeMeter.Tests +{ + using static JmeterDsl; + + public class BlazeMeterEngineTests + { + private TextWriter originalConsoleOut; + + // Redirecting output to progress to get live stdout with nunit. + // https://github.com/nunit/nunit3-vs-adapter/issues/343 + // https://github.com/nunit/nunit/issues/1139 + [SetUp] + public void SetUp() + { + originalConsoleOut = Console.Out; + Console.SetOut(TestContext.Progress); + } + + [TearDown] + public void TearDown() => + Console.SetOut(originalConsoleOut!); + + [Test] + public void TestInBlazeMeter() + { + var stats = TestPlan( + ThreadGroup(1, 1, + HttpSampler("http://localhost") + ) + ).RunIn(new BlazeMeterEngine(Environment.GetEnvironmentVariable("BZ_TOKEN"))); + Assert.That(stats.Overall.ErrorsCount, Is.EqualTo(1)); + } + } +} \ No newline at end of file diff --git a/Abstracta.JmeterDsl.BlazeMeter.Tests/Usings.cs b/Abstracta.JmeterDsl.BlazeMeter.Tests/Usings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/Abstracta.JmeterDsl.BlazeMeter.Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/Abstracta.JmeterDsl.BlazeMeter.Tests/log4j2.xml b/Abstracta.JmeterDsl.BlazeMeter.Tests/log4j2.xml new file mode 100644 index 0000000..e04775a --- /dev/null +++ b/Abstracta.JmeterDsl.BlazeMeter.Tests/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Abstracta.JmeterDsl.BlazeMeter/Abstracta.JmeterDsl.BlazeMeter.csproj b/Abstracta.JmeterDsl.BlazeMeter/Abstracta.JmeterDsl.BlazeMeter.csproj new file mode 100644 index 0000000..771bb07 --- /dev/null +++ b/Abstracta.JmeterDsl.BlazeMeter/Abstracta.JmeterDsl.BlazeMeter.csproj @@ -0,0 +1,50 @@ + + + + netstandard2.0 + Abstracta.JmeterDsl.BlazeMeter + ../StyleCop.ruleset + true + true + Abstracta.JmeterDsl.BlazeMeter + Abstracta.JmeterDsl.BlazeMeter + Abstracta + Abstracta + Module which allows to easily run Abstracta.JmeterDsl test plans at scale in BlazeMeter. + true + jmeter,performance,load,test + logo.png + Apache-2.0 + https://abstracta.github.io/jmeter-dotnet-dsl + README.md + true + snupkg + true + 0.1.0.0 + 0.1 + 0.1 + + + + true + + + + + + + + + + + + + + + + + + + + + diff --git a/Abstracta.JmeterDsl.BlazeMeter/BlazeMeterEngine.cs b/Abstracta.JmeterDsl.BlazeMeter/BlazeMeterEngine.cs new file mode 100644 index 0000000..14cb81b --- /dev/null +++ b/Abstracta.JmeterDsl.BlazeMeter/BlazeMeterEngine.cs @@ -0,0 +1,263 @@ +using System; +using Abstracta.JmeterDsl.Core; +using Abstracta.JmeterDsl.Core.Engines; + +namespace Abstracta.JmeterDsl.BlazeMeter +{ + /// + /// A which allows running DslTestPlan in BlazeMeter. + /// + public class BlazeMeterEngine : BaseJmeterEngine + { + private readonly string _authToken; + private string _testName = "jmeter-dotnet-dsl"; + private long? _projectId; + private TimeSpan? _testTimeout; + private TimeSpan? _availableDataTimeout; + private int? _totalUsers; + private TimeSpan? _rampUp; + private int? _iterations; + private TimeSpan? _holdFor; + private int? _threadsPerEngine; + private bool? _useDebugRun; + + /// + /// Builds a new instance of BlazeMeterEngine with provided authentication token. + /// + /// is the authentication token to be used to access BlazeMeter API. + ///
+ /// It follows the following format: <Key ID>:<Key Secret>. + ///
+ /// Check BlazeMeter + /// API keys for instructions on how to generate them. + /// + public BlazeMeterEngine(string authToken) + { + _authToken = authToken; + } + + /// + /// Sets the name of the BlazeMeter test to use. + ///
+ /// BlazeMeterEngine will search for a test with the given name in the given project (Check + /// ) and if one exists, it will update it and use it to run the provided + /// test plan. If a test with the given name does not exist, then it will create a new one to run + /// the given test plan. + ///
+ /// When not specified, the test name defaults to "jmeter-dotnet-dsl". + ///
+ /// specifies the name of the test to update or create in BlazeMeter. + /// the engine for further configuration or usage. + public BlazeMeterEngine TestName(string testName) + { + _testName = testName; + return this; + } + + /// + /// Specifies the ID of the BlazeMeter project where to run the test. + ///
+ /// You can get the ID of the project by selecting a given project in BlazeMeter and getting the + /// number right after "/projects" in the URL. + ///
+ /// When no project ID is specified, then the default one for the user (associated to the given + /// authentication token) is used. + ///
+ /// is the ID of the project to be used to run the test. + /// the engine for further configuration or usage. + public BlazeMeterEngine ProjectId(long projectId) + { + _projectId = projectId; + return this; + } + + /// + /// Specifies a timeout for the entire test execution. + ///
+ /// If the timeout is reached then the test run will throw a JvmException. + ///
+ /// It is strongly advised to set this timeout properly in each run, according to the expected test + /// execution time plus some additional margin (to consider for additional delays in BlazeMeter + /// test setup and teardown). + ///
+ /// This timeout exists to avoid any potential problem with BlazeMeter execution not detected by + /// the client, and avoid keeping the test indefinitely running until is interrupted by a user. + /// This is specially annoying when running tests in automated fashion, for example in CI/CD. + ///
+ /// When not specified, the default timeout will is set to 1 hour. + ///
+ /// to be used as time limit for test execution. If execution takes more than + /// this, then a JvmException will be thrown by the engine. + /// the engine for further configuration or usage. + public BlazeMeterEngine TestTimeout(TimeSpan testTimeout) + { + _testTimeout = testTimeout; + return this; + } + + /// + /// Specifies a timeout for waiting for test data (metrics) to be available in BlazeMeter. + ///
+ /// After a test is marked as ENDED in BlazeMeter, it may take a few seconds for the associated + /// final metrics to be available. In some cases, the test is marked as ENDED by BlazeMeter, but + /// the data is never available. This usually happens when there is some problem running the test + /// (for example some internal problem with BlazeMeter engine, some missing jmeter plugin, or some + /// other jmeter error). This timeout makes sure that tests properly fail (throwing a + /// JvmException) when they are marked as ENDED and no data is available after the given + /// timeout, and avoids unnecessary wait for test execution timeout. + ///
+ /// Usually this timeout should not be necessary to change, but the API provides such method in + /// case you need to tune such setting. + ///
+ /// When not specified, this value will default to 30 seconds. + ///
+ /// to wait for available data after a test ends, before throwing a JvmException + /// the engine for further configuration or usage. + public BlazeMeterEngine AvailableDataTimeout(TimeSpan availableDataTimeout) + { + _availableDataTimeout = availableDataTimeout; + return this; + } + + /// + /// Specifies the number of virtual users to use when running the test. + ///
+ /// This value overwrites any value specified in JMeter test plans thread groups. + ///
+ /// When no configuration is given for TotalUsers, RampUpFor, Iterations or HoldFor, then + /// configuration will be taken from the first default thread group found in the test plan. + /// Otherwise, when no totalUsers is specified, 1 total user for will be used. + ///
+ /// number of virtual users to run the test with. + /// the engine for further configuration or usage. + public BlazeMeterEngine TotalUsers(int totalUsers) + { + _totalUsers = totalUsers; + return this; + } + + /// + /// Sets the duration of time taken to start the specified total users. + ///
+ /// For example if TotalUsers is set to 10, RampUp is 1 minute and HoldFor is 10 minutes, it means + /// that it will take 1 minute to start the 10 users (starting them in a linear fashion: 1 user + /// every 6 seconds), and then continue executing the test with the 10 users for 10 additional + /// minutes. + ///
+ /// This value overwrites any value specified in JMeter test plans thread groups. + ///
+ /// Take into consideration that BlazeMeter does not support specifying this value in units more + /// granular than minutes, so, if you use a finer grain duration, it will be rounded up to minutes + /// (eg: if you specify 61 seconds, this will be translated into 2 minutes). + ///
+ /// When no configuration is given for TotalUsers, RampUpFor, Iterations or HoldFor, then + /// configuration will be taken from the first default thread group found in the test plan. + /// Otherwise, when no ramp up is specified, 0 ramp up will be used. + ///
+ /// duration that BlazeMeter will take to spin up all the virtual users. + /// the engine for further configuration or usage. + public BlazeMeterEngine RampUpFor(TimeSpan rampUp) + { + _rampUp = rampUp; + return this; + } + + /// + /// Specifies the number of iterations each virtual user will execute. + ///
+ /// If both Iterations and HoldFor are specified, then iterations are ignored and only HoldFor is + /// taken into consideration. + ///
+ /// When neither Iterations and HoldFor are specified, then the last test run configuration is + /// used, or the criteria specified in the JMeter test plan if no previous test run exists. + ///
+ /// When no configuration is given for TotalUsers, RampUpFor, Iterations or HoldFor, then + /// configuration will be taken from the first default thread group found in the test plan. + /// Otherwise, when no iterations are specified, infinite iterations will be used. + ///
+ /// for each virtual users to execute. + /// the engine for further configuration or usage. + public BlazeMeterEngine Iterations(int iterations) + { + _iterations = iterations; + return this; + } + + /// + /// Specifies the duration of time to keep the virtual users running, after the rampUp period. + ///
+ /// If both Iterations and HoldFor are specified, then Iterations are ignored and only HoldFor is + /// taken into consideration. + ///
+ /// When neither Iterations and HoldFor are specified, then the last test run configuration is + /// used, or the criteria specified in the JMeter test plan if no previous test run exists. + ///
+ /// Take into consideration that BlazeMeter does not support specifying this value in units more + /// granular than minutes, so, if you use a finer grain duration, it will be rounded up to minutes + /// (eg: if you specify 61 seconds, this will be translated into 2 minutes). + ///
+ /// When no configuration is given for TotalUsers, RampUpFor, Iterations or HoldFor, then + /// configuration will be taken from the first default thread group found in the test plan. + /// Otherwise, when no hold for or iterations are specified, 10 seconds hold for will be used. + ///
+ /// duration to keep virtual users running after the RampUp period. + /// the engine for further configuration or usage. + public BlazeMeterEngine HoldFor(TimeSpan holdFor) + { + _holdFor = holdFor; + return this; + } + + /// + /// Specifies the number of threads/virtual users to use per BlazeMeter engine (host or + /// container). + ///
+ /// It is always important to use as less resources (which reduces costs) as possible to generate + /// the required load for the test. Too few resources might lead to misguiding results, since the + /// instances/engines running might be saturating and not properly imposing the expected load upon + /// the system under test. Too many resources might lead to unnecessary expenses (wasted money). + ///
+ /// This setting, in conjunction with TotalUsers, determines the number of engines BlazeMeter will + /// use to run the test. For example, if you specify TotalUsers to 500 and 100 ThreadsPerEngine, + /// then 5 engines will be used to run the test. + ///
+ /// It is important to set this value appropriately, since different test plans may impose + /// different load in BlazeMeter engines. This in turns ends up defining different limit of number + /// of virtual users per engine that a test run requires to properly measure the performance of the + /// system under test. This process is usually referred as "calibration" and you can read more + /// about it here. + ///
+ /// When not specified, the value of the last test run will be used, or the default one for your + /// BlazeMeter billing plan if no previous test run exists. + ///
+ /// the number of threads/virtual users to execute per BlazeMeter engine. + /// the engine for further configuration or usage. + public BlazeMeterEngine ThreadsPerEngine(int threadsPerEngine) + { + _threadsPerEngine = threadsPerEngine; + return this; + } + + /// + /// Specifies that the test run will use BlazeMeter debug run feature, not consuming credits but + /// limited up to 10 threads and 5 minutes or 100 iterations. + /// + /// the engine for further configuration or usage. + public BlazeMeterEngine UseDebugRun() => + UseDebugRun(true); + + /// + /// Same as but allowing to enable or disable the settign. + ///
+ /// This is helpful when the resolution is taken at runtime. + ///
+ /// enable specifies to enable or disable the setting. By default, it is set to false. + /// the engine for further configuration or usage. + /// + public BlazeMeterEngine UseDebugRun(bool enable) + { + _useDebugRun = enable; + return this; + } + } +} diff --git a/Abstracta.JmeterDsl.BlazeMeter/pom.xml b/Abstracta.JmeterDsl.BlazeMeter/pom.xml new file mode 100644 index 0000000..d48e98f --- /dev/null +++ b/Abstracta.JmeterDsl.BlazeMeter/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + us.abstracta.jmeter.dotnet + jmeter-dotnet-dsl-parent + 1.0-SNAPSHOT + ../pom.xml + + jmeter-dotnet-dsl-blazemeter + pom + + This pom is only needed to be able to copy jmeter-java-dsl-blazmeter jar and its dependencies with dependency:copy-dependencies maven plugin goal + + + + us.abstracta.jmeter + jmeter-java-dsl-blazemeter + ${jmeter-java-dsl.version} + + + us.abstracta.jmeter + jmeter-java-dsl + + + + + \ No newline at end of file diff --git a/Abstracta.JmeterDsl.Tests/Abstracta.JmeterDsl.Tests.csproj b/Abstracta.JmeterDsl.Tests/Abstracta.JmeterDsl.Tests.csproj new file mode 100644 index 0000000..63d7234 --- /dev/null +++ b/Abstracta.JmeterDsl.Tests/Abstracta.JmeterDsl.Tests.csproj @@ -0,0 +1,48 @@ + + + net6.0 + Abstracta.JmeterDsl + false + ../StyleCop.ruleset + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/Abstracta.JmeterDsl.Tests/Core/Listeners/JtlWriterTest.cs b/Abstracta.JmeterDsl.Tests/Core/Listeners/JtlWriterTest.cs new file mode 100644 index 0000000..92523c7 --- /dev/null +++ b/Abstracta.JmeterDsl.Tests/Core/Listeners/JtlWriterTest.cs @@ -0,0 +1,34 @@ +using System.IO; + +namespace Abstracta.JmeterDsl.Core.Listeners +{ + using static JmeterDsl; + + public class JtlWriterTest + { + [Test] + public void ShouldWriteResultsToFileWhenJtlWriterAtTestPlan() + { + DirectoryInfo workDir = new DirectoryInfo("jtls"); + try + { + TestPlan( + ThreadGroup(1, 1, + DummySampler("ok"), + JtlWriter(workDir.FullName) + )).Run(); + Assert.That(GetFileLinesCount(FindJtlFileInDirectory(workDir)), Is.EqualTo(2)); + } + finally + { + workDir.Delete(true); + } + } + + private FileInfo FindJtlFileInDirectory(DirectoryInfo workDir) => + workDir.GetFiles("*.jtl")[0]; + + private int GetFileLinesCount(FileInfo jtlFile) => + File.ReadAllLines(jtlFile.FullName).Length; + } +} diff --git a/Abstracta.JmeterDsl.Tests/Core/Listeners/ResponseFileSaverTest.cs b/Abstracta.JmeterDsl.Tests/Core/Listeners/ResponseFileSaverTest.cs new file mode 100644 index 0000000..79458b3 --- /dev/null +++ b/Abstracta.JmeterDsl.Tests/Core/Listeners/ResponseFileSaverTest.cs @@ -0,0 +1,30 @@ +using System.IO; + +namespace Abstracta.JmeterDsl.Core.Listeners +{ + using static JmeterDsl; + + public class ResponseFileSaverTest + { + [Test] + public void ShouldWriteFileWithResponseContentWhenResponseFileSaverInPlan() + { + var workDir = new DirectoryInfo("responses"); + try + { + var body = "ok"; + TestPlan( + ThreadGroup(1, 1, + DummySampler(body), + ResponseFileSaver(Path.Combine(workDir.FullName, "response")) + )).Run(); + var responseFileBody = File.ReadAllText(Path.Combine(workDir.FullName, "response1.unknown")); + Assert.That(responseFileBody, Is.EqualTo(body)); + } + finally + { + workDir.Delete(true); + } + } + } +} diff --git a/Abstracta.JmeterDsl.Tests/Core/ThreadGroups/DslThreadGroupTest.cs b/Abstracta.JmeterDsl.Tests/Core/ThreadGroups/DslThreadGroupTest.cs new file mode 100644 index 0000000..21aeb13 --- /dev/null +++ b/Abstracta.JmeterDsl.Tests/Core/ThreadGroups/DslThreadGroupTest.cs @@ -0,0 +1,32 @@ +using System; + +namespace Abstracta.JmeterDsl.Core.ThreadGroups +{ + using static JmeterDsl; + + public class DslThreadGroupTest + { + [Test] + public void ShouldMakeOneRequestWhenOneThreadAndIteration() + { + var stats = TestPlan( + ThreadGroup(threads: 1, iterations: 1, + DummySampler("OK") + ) + ).Run(); + Assert.That(stats.Overall.SamplesCount, Is.EqualTo(1)); + } + + [Test] + public void ShouldTakeAtLeastDurationWhenThreadGroupWithDuration() + { + var duration = TimeSpan.FromSeconds(5); + var stats = TestPlan( + ThreadGroup(threads: 1, duration: duration, + DummySampler("OK") + ) + ).Run(); + Assert.That(stats.Duration, Is.GreaterThanOrEqualTo(duration)); + } + } +} diff --git a/Abstracta.JmeterDsl.Tests/Http/DslHttpSamplerTest.cs b/Abstracta.JmeterDsl.Tests/Http/DslHttpSamplerTest.cs new file mode 100644 index 0000000..7981ceb --- /dev/null +++ b/Abstracta.JmeterDsl.Tests/Http/DslHttpSamplerTest.cs @@ -0,0 +1,139 @@ +using System.Net.Http.Headers; +using System.Net.Mime; +using WireMock.FluentAssertions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace Abstracta.JmeterDsl.Http +{ + using static JmeterDsl; + + public class DslHttpSamplerTest + { + private WireMockServer _wiremock; + + [SetUp] + public void SetUp() + { + _wiremock = WireMockServer.Start(); + _wiremock.Given(Request.Create().WithPath("/")) + .RespondWith(Response.Create() + .WithStatusCode(200)); + } + + [TearDown] + public void TearDown() => + _wiremock.Stop(); + + [Test] + public void ShouldMakeHttpRequestWhenSimpleHttpGet() + { + TestPlan( + ThreadGroup(threads: 1, iterations: 1, + HttpSampler(_wiremock.Url) + ) + ).Run(); + _wiremock.Should().HaveReceivedACall().UsingGet(); + } + + [Test] + public void ShouldMakeHttpRequestWithBodyAndHeadersWhenHttpPost() + { + var customHeaderName = "X-Test"; + var customHeaderValue = "Val"; + var requestBody = "{\"prop\": \"val\"}"; + TestPlan( + ThreadGroup(threads: 1, iterations: 1, + HttpSampler(_wiremock.Url) + .Post(requestBody, new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) + .Header(customHeaderName, customHeaderValue) + ) + ).Run(); + _wiremock.Should() + .HaveReceivedACall() + .UsingPost() + .And + .WithHeader("Content-Type", "application/json") + .And + .WithHeader(customHeaderName, customHeaderValue) + .And + .WithBody(requestBody); + } + + [Test] + public void ShouldMakeHttpRequestWithHeaderWhenHeaderAtTestPlanLevel() + { + var customHeaderName = "X-Test"; + var customHeaderValue = "Val"; + TestPlan( + HttpHeaders() + .Header(customHeaderName, customHeaderValue), + ThreadGroup(threads: 1, iterations: 1, + HttpSampler(_wiremock.Url) + ) + ).Run(); + _wiremock.Should() + .HaveReceivedACall() + .WithHeader(customHeaderName, customHeaderValue); + } + + [Test] + public void ShouldNotKeepCookiesWhenDisabled() + { + SetupHttpResponseWithCookie(); + TestPlan( + HttpCookies().Disable(), + ThreadGroup(1, 1, + HttpSampler(_wiremock.Url), + HttpSampler(_wiremock.Url) + ) + ).Run(); + _wiremock.Should() + .HaveReceivedACall() + .WithoutHeader("Cookie"); + } + + private void SetupHttpResponseWithCookie() + { + _wiremock.Given(Request.Create().WithPath("/")) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithHeader("Set-Cookie", "MyCookie=val")); + } + + [Test] + public void ShouldNotUseCacheWhenDisabled() + { + SetupCacheableHttpResponse(); + TestPlan( + BuildHeadersToFixHttpCaching(), + HttpCache().Disable(), + ThreadGroup(1, 1, + HttpSampler(_wiremock.Url), + HttpSampler(_wiremock.Url) + ) + ).Run(); + _wiremock.Should() + .HaveReceived(2) + .Calls() + .UsingGet(); + } + + private void SetupCacheableHttpResponse() + { + _wiremock.Given(Request.Create().WithPath("/")) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithHeader("Cache-Control", "max-age=600")); + } + + /* + need to set header for request header to match otherwise jmeter automatically adds this + header while sending request and stores it in cache and when it checks in next request + it doesn't match since same header is not yet set at check time. + */ + private HttpHeaders BuildHeadersToFixHttpCaching() => + HttpHeaders().Header("User-Agent", "jmeter-java-dsl"); + } +} diff --git a/Abstracta.JmeterDsl.Tests/Usings.cs b/Abstracta.JmeterDsl.Tests/Usings.cs new file mode 100644 index 0000000..4ea06d0 --- /dev/null +++ b/Abstracta.JmeterDsl.Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/Abstracta.JmeterDsl.Tests/WireMockAssertionsExtensions.cs b/Abstracta.JmeterDsl.Tests/WireMockAssertionsExtensions.cs new file mode 100644 index 0000000..12f8149 --- /dev/null +++ b/Abstracta.JmeterDsl.Tests/WireMockAssertionsExtensions.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using FluentAssertions; +using FluentAssertions.Execution; +using WireMock; +using WireMock.FluentAssertions; +using WireMock.Types; + +namespace Abstracta.JmeterDsl +{ + public static class WireMockAssertionsExtensions + { + public static AndConstraint WithBody(this WireMockAssertions instance, string body) + { + var requestsField = GetPrivateField("_requestMessages", instance); + var requests = (IReadOnlyList)requestsField.GetValue(instance)!; + var callsCount = (int?)GetPrivateField("_callsCount", instance).GetValue(instance); + Func predicate = request => string.Equals(request.Body, body, StringComparison.OrdinalIgnoreCase); + Func, IReadOnlyList> filter = requests => requests.Where(predicate).ToList(); + Func, bool> condition = requests => (callsCount is null && filter(requests).Any()) || callsCount == filter(requests).Count; + + Execute.Assertion + .BecauseOf(string.Empty, Array.Empty()) + .Given(() => requests) + .ForCondition(requests => callsCount == 0 || requests.Any()) + .FailWith( + "Expected {context:wiremockserver} to have been called using body {0}{reason}, but no calls were made.", + body + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockserver} to have been called using body {0}{reason}, but didn't find it among the bodies {1}.", + _ => body, + requests => requests.Select(request => request.Body) + ); + requestsField.SetValue(instance, filter(requests).ToList()); + return new AndConstraint(instance); + } + + private static FieldInfo GetPrivateField(string fieldName, object o) => + o.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance)!; + + public static AndConstraint WithoutHeader(this WireMockAssertions instance, string headerName) + { + var headersField = GetPrivateField("_headers", instance); + var headers = (IReadOnlyList>>)headersField.GetValue(instance)!; + using (new AssertionScope("headers from requests sent")) + { + headers.Select(h => h.Key).Should().NotContain(headerName); + } + return new AndConstraint(instance); + } + } +} diff --git a/Abstracta.JmeterDsl.Tests/log4j2.xml b/Abstracta.JmeterDsl.Tests/log4j2.xml new file mode 100644 index 0000000..e04775a --- /dev/null +++ b/Abstracta.JmeterDsl.Tests/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Abstracta.JmeterDsl.sln b/Abstracta.JmeterDsl.sln new file mode 100644 index 0000000..2f39cff --- /dev/null +++ b/Abstracta.JmeterDsl.sln @@ -0,0 +1,61 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 25.0.1705.4 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl", "Abstracta.JmeterDsl\Abstracta.JmeterDsl.csproj", "{52A68F2B-F431-49FF-8518-265238BF62FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl.Tests", "Abstracta.JmeterDsl.Tests\Abstracta.JmeterDsl.Tests.csproj", "{0BC6F8BE-9959-49AA-97BD-1AD3EF2C3FB6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D722220C-CD82-4457-984E-DEE739CE632C}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + StyleCop.ruleset = StyleCop.ruleset + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl.Azure", "Abstracta.JmeterDsl.Azure\Abstracta.JmeterDsl.Azure.csproj", "{5F7E31FB-CCC9-4FFC-ACD3-20D47B91A188}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl.Azure.Tests", "Abstracta.JmeterDsl.Azure.Tests\Abstracta.JmeterDsl.Azure.Tests.csproj", "{8EE1AA42-6DD8-4A24-A9E0-0549E9B2F9B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl.BlazeMeter", "Abstracta.JmeterDsl.BlazeMeter\Abstracta.JmeterDsl.BlazeMeter.csproj", "{B998F9F5-DC15-4FE3-9624-F0BB8E6B3591}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl.BlazeMeter.Tests", "Abstracta.JmeterDsl.BlazeMeter.Tests\Abstracta.JmeterDsl.BlazeMeter.Tests.csproj", "{9170BA21-A016-402B-9E30-034937B33DB8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {52A68F2B-F431-49FF-8518-265238BF62FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52A68F2B-F431-49FF-8518-265238BF62FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52A68F2B-F431-49FF-8518-265238BF62FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52A68F2B-F431-49FF-8518-265238BF62FF}.Release|Any CPU.Build.0 = Release|Any CPU + {0BC6F8BE-9959-49AA-97BD-1AD3EF2C3FB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0BC6F8BE-9959-49AA-97BD-1AD3EF2C3FB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0BC6F8BE-9959-49AA-97BD-1AD3EF2C3FB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0BC6F8BE-9959-49AA-97BD-1AD3EF2C3FB6}.Release|Any CPU.Build.0 = Release|Any CPU + {5F7E31FB-CCC9-4FFC-ACD3-20D47B91A188}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F7E31FB-CCC9-4FFC-ACD3-20D47B91A188}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F7E31FB-CCC9-4FFC-ACD3-20D47B91A188}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F7E31FB-CCC9-4FFC-ACD3-20D47B91A188}.Release|Any CPU.Build.0 = Release|Any CPU + {8EE1AA42-6DD8-4A24-A9E0-0549E9B2F9B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EE1AA42-6DD8-4A24-A9E0-0549E9B2F9B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EE1AA42-6DD8-4A24-A9E0-0549E9B2F9B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EE1AA42-6DD8-4A24-A9E0-0549E9B2F9B5}.Release|Any CPU.Build.0 = Release|Any CPU + {B998F9F5-DC15-4FE3-9624-F0BB8E6B3591}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B998F9F5-DC15-4FE3-9624-F0BB8E6B3591}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B998F9F5-DC15-4FE3-9624-F0BB8E6B3591}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B998F9F5-DC15-4FE3-9624-F0BB8E6B3591}.Release|Any CPU.Build.0 = Release|Any CPU + {9170BA21-A016-402B-9E30-034937B33DB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9170BA21-A016-402B-9E30-034937B33DB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9170BA21-A016-402B-9E30-034937B33DB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9170BA21-A016-402B-9E30-034937B33DB8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {152E808D-363F-4C3D-BDF9-D9DFA65E05AE} + EndGlobalSection +EndGlobal diff --git a/Abstracta.JmeterDsl/Abstracta.JmeterDsl.csproj b/Abstracta.JmeterDsl/Abstracta.JmeterDsl.csproj new file mode 100644 index 0000000..3506c18 --- /dev/null +++ b/Abstracta.JmeterDsl/Abstracta.JmeterDsl.csproj @@ -0,0 +1,68 @@ + + + netstandard2.0 + Abstracta.JmeterDsl + ../StyleCop.ruleset + true + true + Abstracta.JmeterDsl + Abstracta.JmeterDsl + Abstracta + Abstracta + Simple API to run JMeter performance tests in an VCS and programmers friendly way. + true + jmeter,performance,load,test + logo.png + Apache-2.0 + https://abstracta.github.io/jmeter-dotnet-dsl + README.md + true + snupkg + true + 0.1.0.0 + 0.1 + 0.1 + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + \ No newline at end of file diff --git a/Abstracta.JmeterDsl/Core/Bridge/BridgeService.cs b/Abstracta.JmeterDsl/Core/Bridge/BridgeService.cs new file mode 100644 index 0000000..297a4a2 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Bridge/BridgeService.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Abstracta.JmeterDsl.Core.Bridge +{ + public class BridgeService + { + private string _jvmArgs = string.Empty; + + public BridgeService JvmArgs(string args) + { + _jvmArgs = args; + return this; + } + + public TestPlanStats RunTestPlanInEngine(DslTestPlan testPlan, IDslJmeterEngine engine) + { + var executionId = Guid.NewGuid().ToString(); + var tempDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), executionId)); + tempDir.Create(); + try + { + var responseFile = new FileInfo(Path.Combine(tempDir.FullName, "stats.yml")); + RunBridgeCommand("run", new TestPlanExecution(engine, testPlan), $"\"{responseFile.FullName}\""); + return responseFile.Exists ? ParseResponse(responseFile) : null; + } + finally + { + tempDir.Delete(true); + } + } + + private void RunBridgeCommand(string command, object testElement, string args) + { + var classPath = SolveClassPath(testElement); + var mainClass = "us.abstracta.jmeter.javadsl.bridge.BridgeService"; + var jvmArgs = _jvmArgs; + var log4jConfigFile = new FileInfo("log4j2.xml"); + if (log4jConfigFile.Exists) + { + jvmArgs += $" -Dlog4j2.configurationFile=\"{log4jConfigFile.FullName}\""; + } + jvmArgs += $" -Dus.abstracta.jmeterdsl.userAgent=jmeter-dotnet-dsl/{Assembly.GetExecutingAssembly().GetName().Version}"; + using (var process = StartJvmProcess($"{jvmArgs} -cp \"{classPath}\" {mainClass} {command}" + (args != null ? " " + args : string.Empty))) + { + using (var stdin = process.StandardInput) + { + SerializeObjectToWriter(testElement, stdin); + } + WaitJvmProcessExit(process); + } + } + + private void SerializeObjectToWriter(object val, TextWriter writer) + { + var testElementConverter = new BridgedObjectConverter(); + var builder = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(testElementConverter) + .WithTypeConverter(new TimespanConverter()); + testElementConverter.ValueSerializer = builder.BuildValueSerializer(); + builder.Build().Serialize(writer, val); + } + + private string SolveClassPath(object testElement) + { + var jarsDir = SolveJarsDir(); + if (JarsDirRequiresUpdate(jarsDir)) + { + if (jarsDir.Exists) + { + jarsDir.Delete(true); + } + jarsDir.Create(); + } + SortedSet jars = new SortedSet(); + CopyRequiredJarsToDir(testElement, jarsDir, jars, new HashSet(), new HashSet()); + return string.Join(SolveClassPathSeparator(), jars); + } + + private DirectoryInfo SolveJarsDir() + { + var envHome = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "HOMEPATH" : "HOME"; + var homePath = Environment.GetEnvironmentVariable(envHome); + return new DirectoryInfo(Path.Combine(homePath, ".jmeter-dsl", "jars")); + } + + private bool JarsDirRequiresUpdate(DirectoryInfo jarsDir) + { + var jarName = ExtractJarNameFromResourceName(Assembly.GetExecutingAssembly().GetManifestResourceNames()[0]); + return !new FileInfo(Path.Combine(jarsDir.FullName, jarName)).Exists; + } + + private string ExtractJarNameFromResourceName(string resourceName) + { + var prefixDelimiter = ".artifacts."; + var delimiterPos = resourceName.IndexOf(prefixDelimiter); + return delimiterPos >= 0 && resourceName.EndsWith(".jar") ? resourceName.Substring(delimiterPos + prefixDelimiter.Length) : null; + } + + private string SolveClassPathSeparator() => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":"; + + private void CopyRequiredJarsToDir(object element, DirectoryInfo dir, SortedSet jars, + ISet processedAssemblies, ISet processedTypes) + { + var elementType = element.GetType(); + if (!processedTypes.Add(elementType.FullName)) + { + return; + } + var assembly = Assembly.GetAssembly(elementType); + if (assembly == null || !processedAssemblies.Add(assembly.FullName)) + { + return; + } + CopyAssemblyJarsToDir(assembly, dir, jars); + var fields = element.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance); + foreach (FieldInfo field in fields) + { + var fieldValue = field.GetValue(element); + if (fieldValue != null) + { + CopyRequiredJarsToDir(fieldValue, dir, jars, processedAssemblies, processedTypes); + } + } + } + + private void CopyAssemblyJarsToDir(Assembly assembly, DirectoryInfo dir, SortedSet jars) + { + foreach (var r in assembly.GetManifestResourceNames()) + { + CopyAssemblyJarResourceToDir(r, assembly, dir, jars); + } + } + + private void CopyAssemblyJarResourceToDir(string resourceName, Assembly assembly, DirectoryInfo dir, SortedSet jars) + { + var jarName = ExtractJarNameFromResourceName(resourceName); + if (jarName == null) + { + return; + } + var targetFile = new FileInfo(Path.Combine(dir.FullName, jarName)); + if (!targetFile.Exists) + { + using (var targetFileStream = targetFile.Create()) + { + assembly.GetManifestResourceStream(resourceName).CopyTo(targetFileStream); + } + } + jars.Add(targetFile.FullName); + } + + private Process StartJvmProcess(string jvmArgs) + { + var process = new Process(); + ProcessStartInfo startInfo = new ProcessStartInfo() + { + FileName = "java", + Arguments = jvmArgs, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + }; + process.StartInfo = startInfo; + process.OutputDataReceived += (sender, e) => Console.WriteLine(e.Data); + process.ErrorDataReceived += (sender, e) => Console.Error.WriteLine(e.Data); + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + return process; + } + + private void WaitJvmProcessExit(Process process) + { + process.WaitForExit(); + if (process.ExitCode != 0) + { + throw new JvmException(); + } + } + + private T ParseResponse(FileInfo responseFile) + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new TimespanConverter()) + .Build(); + using (var reader = new StreamReader(responseFile.FullName)) + { + return deserializer.Deserialize(reader); + } + } + + public void SaveTestPlanAsJmx(DslTestPlan testPlan, string filePath) => + RunBridgeCommand("saveAsJmx", testPlan, $"\"{filePath}\""); + + public void ShowTestElementInGui(IDslTestElement testElement) => + RunBridgeCommand("showInGui", testElement, null); + } +} diff --git a/Abstracta.JmeterDsl/Core/Bridge/BridgedObjectConverter.cs b/Abstracta.JmeterDsl/Core/Bridge/BridgedObjectConverter.cs new file mode 100644 index 0000000..f526654 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Bridge/BridgedObjectConverter.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Abstracta.JmeterDsl.Core.Bridge +{ + public class BridgedObjectConverter : IYamlTypeConverter + { + private const string DslClassPrefix = "Dsl"; + private const string NamespacePrefix = "Abstracta.JmeterDsl."; + + public IValueSerializer ValueSerializer { get; set; } + + public bool Accepts(Type type) => + typeof(IDslTestElement).IsAssignableFrom(type) + || typeof(IDslJmeterEngine).IsAssignableFrom(type) + || typeof(TestPlanExecution).IsAssignableFrom(type); + + public object ReadYaml(IParser parser, Type type) => + throw new NotImplementedException(); + + public void WriteYaml(IEmitter emitter, object value, Type type) + { + var valueType = value.GetType(); + var tagName = BuildTagName(valueType); + emitter.Emit(new MappingStart(null, tagName, false, MappingStyle.Any)); + var fields = valueType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); + if (fields.Length == 1 && typeof(IDictionary).IsAssignableFrom(fields[0].FieldType)) + { + WriteDictionaryYaml((IDictionary)fields[0].GetValue(value), emitter); + } + else + { + foreach (FieldInfo field in fields) + { + if (!IsIgnoredField(field)) + { + WriteFieldYaml(field, field.GetValue(value), emitter); + } + } + } + emitter.Emit(new MappingEnd()); + } + + private string BuildTagName(Type valueType) => + "!" + (IsCoreElement(valueType) ? BuildSimpleTagName(valueType) : BuildCompleteTagName(valueType)); + + private bool IsCoreElement(Type valueType) + { + var typeFullName = valueType.FullName; + return typeFullName.StartsWith(NamespacePrefix + "Core.") + || typeFullName.StartsWith(NamespacePrefix + "Http.") + || typeFullName.StartsWith(NamespacePrefix + "Java."); + } + + private string BuildSimpleTagName(Type valueType) + { + var ret = valueType.Name; + if (ret.StartsWith(DslClassPrefix)) + { + ret = ret.Substring(DslClassPrefix.Length); + } + ret = LowerFirstChar(ret); + return ret; + } + + private string LowerFirstChar(string val) => + val.Substring(0, 1).ToLower() + val.Substring(1); + + private string BuildCompleteTagName(Type valueType) + { + var ret = valueType.FullName; + if (ret.StartsWith(NamespacePrefix)) + { + ret = ret.Substring(NamespacePrefix.Length); + } + return LowerNamespaces(ret); + } + + private string LowerNamespaces(string fullName) + { + var ret = string.Empty; + int start = 0; + int end = fullName.IndexOf('.'); + while (end != -1) + { + ret += fullName.Substring(start, end - start + 1).ToLower(); + start = end + 1; + end = fullName.IndexOf('.', start); + } + ret += fullName.Substring(start); + return ret; + } + + private void WriteDictionaryYaml(IDictionary value, IEmitter emitter) + { + foreach (KeyValuePair entry in value) + { + emitter.Emit(new Scalar(entry.Key)); + emitter.Emit(new Scalar(entry.Value)); + } + } + + private bool IsIgnoredField(FieldInfo field) => + field.GetCustomAttribute(typeof(YamlIgnoreAttribute)) != null; + + private void WriteFieldYaml(FieldInfo field, object value, IEmitter emitter) + { + if (value == null || (value is ICollection collection && collection.Count == 0)) + { + return; + } + + // removing first character since fields are prefixed with underscore + emitter.Emit(new Scalar(field.Name.Substring(1))); + ValueSerializer.SerializeValue(emitter, value, field.FieldType); + } + } +} diff --git a/Abstracta.JmeterDsl/Core/Bridge/JvmException.cs b/Abstracta.JmeterDsl/Core/Bridge/JvmException.cs new file mode 100644 index 0000000..502dffd --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Bridge/JvmException.cs @@ -0,0 +1,17 @@ +using System; + +namespace Abstracta.JmeterDsl.Core.Bridge +{ + /// + /// Exception thrown when there is some problem interacting with Java Virutal Machine or executing a particular command. + ///
+ /// This might be to some exception generated in test plan execution, showing element in GUI, or event issues with JVM initialization. + ///
+ public class JvmException : Exception + { + public JvmException() + : base("JVM execution failed. Check stderr and stdout for additional info.") + { + } + } +} diff --git a/Abstracta.JmeterDsl/Core/Bridge/TestPlanExecution.cs b/Abstracta.JmeterDsl/Core/Bridge/TestPlanExecution.cs new file mode 100644 index 0000000..09b63e7 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Bridge/TestPlanExecution.cs @@ -0,0 +1,14 @@ +namespace Abstracta.JmeterDsl.Core.Bridge +{ + public class TestPlanExecution + { + private readonly IDslJmeterEngine _engine; + private readonly DslTestPlan _testPlan; + + public TestPlanExecution(IDslJmeterEngine engine, DslTestPlan testPlan) + { + _engine = engine; + _testPlan = testPlan; + } + } +} diff --git a/Abstracta.JmeterDsl/Core/Bridge/TimeSpanConverter.cs b/Abstracta.JmeterDsl/Core/Bridge/TimeSpanConverter.cs new file mode 100644 index 0000000..2324f7f --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Bridge/TimeSpanConverter.cs @@ -0,0 +1,76 @@ +using System; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Abstracta.JmeterDsl.Core.Bridge +{ + public class TimespanConverter : IYamlTypeConverter + { + private const string DurationPrefix = "PT"; + + public bool Accepts(Type type) => + type == typeof(TimeSpan); + + public object ReadYaml(IParser parser, Type type) + { + var scalar = parser.Consume(); + var value = scalar.Value; + if (!value.StartsWith(DurationPrefix)) + { + throw new YamlException(scalar.Start, scalar.End, $"No valid duration value '{value}'"); + } + int hours = 0, minutes = 0, seconds = 0, millis = 0; + var lastPos = DurationPrefix.Length; + var unitPos = value.IndexOf('H'); + if (unitPos >= 0) + { + hours = int.Parse(value.Substring(lastPos, unitPos - lastPos)); + lastPos = unitPos + 1; + } + unitPos = value.IndexOf('M'); + if (unitPos >= 0) + { + minutes = int.Parse(value.Substring(lastPos, unitPos - lastPos)); + lastPos = unitPos + 1; + } + unitPos = value.IndexOf('.'); + if (unitPos >= 0) + { + seconds = int.Parse(value.Substring(lastPos, unitPos - lastPos)); + lastPos = unitPos + 1; + var millisStr = value.Substring(lastPos, Math.Min(3, value.Length - 1 - lastPos)); + millis = int.Parse(millisStr.PadLeft(3, '0')); + } + else if (value.Contains("S")) + { + seconds = int.Parse(value.Substring(lastPos, value.Length - lastPos - 1)); + } + return new TimeSpan(0, hours, minutes, seconds, millis); + } + + public void WriteYaml(IEmitter emitter, object value, Type type) + { + var ret = "PT"; + var duration = (TimeSpan)value; + if (duration.Hours > 0) + { + ret += duration.Hours + "H"; + } + if (duration.Minutes > 0) + { + ret += duration.Minutes + "M"; + } + if (duration.Seconds > 0 || duration.Milliseconds > 0) + { + ret += duration.Seconds; + if (duration.Milliseconds > 0) + { + ret += "." + duration.Milliseconds.ToString().PadLeft(3, '0'); + } + ret += "S"; + } + emitter.Emit(new Scalar(ret)); + } + } +} diff --git a/Abstracta.JmeterDsl/Core/Configs/BaseConfigElement.cs b/Abstracta.JmeterDsl/Core/Configs/BaseConfigElement.cs new file mode 100644 index 0000000..2a561f3 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Configs/BaseConfigElement.cs @@ -0,0 +1,15 @@ +using Abstracta.JmeterDsl.Core.TestElements; + +namespace Abstracta.JmeterDsl.Core.Configs +{ + /// + /// Contains common logic for config elements defined by the DSL. + /// + public abstract class BaseConfigElement : BaseTestElement, IMultiLevelTestElement + { + public BaseConfigElement(string name) + : base(name) + { + } + } +} diff --git a/Abstracta.JmeterDsl/Core/DslTestPlan.cs b/Abstracta.JmeterDsl/Core/DslTestPlan.cs new file mode 100644 index 0000000..924ac14 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/DslTestPlan.cs @@ -0,0 +1,41 @@ +using Abstracta.JmeterDsl.Core.Bridge; +using Abstracta.JmeterDsl.Core.Engines; +using Abstracta.JmeterDsl.Core.TestElements; + +namespace Abstracta.JmeterDsl.Core +{ + /// + /// Represents a JMeter test plan, with associated thread groups and other children elements. + /// + public class DslTestPlan : TestElementContainer + { + public DslTestPlan(ITestPlanChild[] children) + : base(null, children) + { + } + + /// + /// Uses to run the test plan. + /// + /// A object containing all statistics of the test plan execution. + public TestPlanStats Run() => + new EmbeddedJmeterEngine().Run(this); + + /// + /// Allows to run the test plan in a given engine. + ///
+ /// This method is just a simple method which provides fluent API to run the test plans in a given + /// engine. + ///
+ /// + public TestPlanStats RunIn(IDslJmeterEngine engine) => + engine.Run(this); + + /// + /// Saves the given test plan as JMX, which allows it to be loaded in JMeter GUI. + /// + /// specifies where to store the JMX of the test plan. + public void SaveAsJmx(string filePath) => + new BridgeService().SaveTestPlanAsJmx(this, filePath); + } +} diff --git a/Abstracta.JmeterDsl/Core/Engines/BaseJmeterEngine.cs b/Abstracta.JmeterDsl/Core/Engines/BaseJmeterEngine.cs new file mode 100644 index 0000000..bac52d1 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Engines/BaseJmeterEngine.cs @@ -0,0 +1,28 @@ +using Abstracta.JmeterDsl.Core.Bridge; +using YamlDotNet.Serialization; + +namespace Abstracta.JmeterDsl.Core.Engines +{ + public abstract class BaseJmeterEngine : IDslJmeterEngine + where T : BaseJmeterEngine + { + [YamlIgnore] + protected string _jvmArgs = string.Empty; + + /// + /// Specifies arguments to be added to JVM command line. + ///
+ /// This is helpful, for example, when debugging JMeter DSL or JMeter code. + ///
+ public T JvmArgs(string args) + { + _jvmArgs = args; + return (T)this; + } + + public TestPlanStats Run(DslTestPlan testPlan) => + new BridgeService() + .JvmArgs(_jvmArgs) + .RunTestPlanInEngine(testPlan, this); + } +} diff --git a/Abstracta.JmeterDsl/Core/Engines/EmbeddedJmeterEngine.cs b/Abstracta.JmeterDsl/Core/Engines/EmbeddedJmeterEngine.cs new file mode 100644 index 0000000..387d60f --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Engines/EmbeddedJmeterEngine.cs @@ -0,0 +1,9 @@ +namespace Abstracta.JmeterDsl.Core.Engines +{ + /// + /// Allows running test plans in an embedded JMeter instance. + /// + public class EmbeddedJmeterEngine : BaseJmeterEngine + { + } +} diff --git a/Abstracta.JmeterDsl/Core/IDslJmeterEngine.cs b/Abstracta.JmeterDsl/Core/IDslJmeterEngine.cs new file mode 100644 index 0000000..8c2bad9 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/IDslJmeterEngine.cs @@ -0,0 +1,17 @@ +namespace Abstracta.JmeterDsl.Core +{ + /// + /// Interface to be implemented by classes allowing to run a DslTestPlan in different engines. + /// + public interface IDslJmeterEngine + { + /// + /// Runs the given test plan obtaining the execution metrics. + ///
+ /// This method blocks execution until the test plan execution ends. + ///
+ /// to run in the JMeter engine. + /// the metrics associated to the run. + TestPlanStats Run(DslTestPlan testPlan); + } +} diff --git a/Abstracta.JmeterDsl/Core/IDslTestElement.cs b/Abstracta.JmeterDsl/Core/IDslTestElement.cs new file mode 100644 index 0000000..28567d8 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/IDslTestElement.cs @@ -0,0 +1,15 @@ +namespace Abstracta.JmeterDsl.Core +{ + /// + /// Interface to be implemented by all elements composing a JMeter test plan. + /// + public interface IDslTestElement + { + /// + /// Shows the test element in it's defined GUI in a popup window. + ///
+ /// This might be handy to visualize the element as it looks in JMeter GUI. + ///
+ void ShowInGui(); + } +} diff --git a/Abstracta.JmeterDsl/Core/ITestPlanChild.cs b/Abstracta.JmeterDsl/Core/ITestPlanChild.cs new file mode 100644 index 0000000..5c6ddc3 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/ITestPlanChild.cs @@ -0,0 +1,10 @@ +namespace Abstracta.JmeterDsl.Core +{ + /// + /// Test elements that can be added directly as test plan children in JMeter should implement this interface. + /// Check for an example. + /// + public interface ITestPlanChild : IDslTestElement + { + } +} diff --git a/Abstracta.JmeterDsl/Core/Listeners/BaseListener.cs b/Abstracta.JmeterDsl/Core/Listeners/BaseListener.cs new file mode 100644 index 0000000..9796706 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Listeners/BaseListener.cs @@ -0,0 +1,12 @@ +using Abstracta.JmeterDsl.Core.TestElements; + +namespace Abstracta.JmeterDsl.Core.Listeners +{ + public abstract class BaseListener : BaseTestElement, IMultiLevelTestElement + { + protected BaseListener() + : base(null) + { + } + } +} diff --git a/Abstracta.JmeterDsl/Core/Listeners/JtlWriter.cs b/Abstracta.JmeterDsl/Core/Listeners/JtlWriter.cs new file mode 100644 index 0000000..698cbcd --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Listeners/JtlWriter.cs @@ -0,0 +1,540 @@ +namespace Abstracta.JmeterDsl.Core.Listeners +{ + /// + /// Allows to generate a result log file (JTL) with data for each sample for a test plan, thread + /// group or sampler, depending on what level of test plan is added. + ///
+ /// If jtlWriter is added at testPlan level it will log information about all samples in the test + /// plan, if added at thread group level it will only log samples for samplers contained within it, + /// if added as a sampler child, then only that sampler samples will be logged. + ///
+ /// By default, this writer will use JMeter default JTL format, a csv with following fields: + /// timeStamp,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage, + /// bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect. You can change the format to + /// XML and specify additional (or remove existing ones) fields to store with provided methods. + ///
+ /// See JMeter listeners doc for + /// more details on JTL format and settings. + ///
+ /// + public class JtlWriter : BaseListener + { + private readonly string _directory; + private readonly string _fileName; + private bool? _withAllFields; + private bool? _saveAsXml; + private bool? _withElapsedTime; + private bool? _withResponseMessage; + private bool? _withSuccess; + private bool? _withSentByteCount; + private bool? _withResponseFilename; + private bool? _withEncoding; + private bool? _withIdleTime; + private bool? _withResponseHeaders; + private bool? _withAssertionResults; + private bool? _withFieldNames; + private bool? _withLabel; + private bool? _withThreadName; + private bool? _withAssertionFailureMessage; + private bool? _withActiveThreadCounts; + private bool? _withLatency; + private bool? _withSampleAndErrorCounts; + private bool? _withRequestHeaders; + private bool? _withResponseData; + private bool? _withTimeStamp; + private bool? _withResponseCode; + private bool? _withDataType; + private bool? _withReceivedByteCount; + private bool? _withUrl; + private bool? _withConnectTime; + private bool? _withHostname; + private bool? _withSamplerData; + private bool? _withSubResults; + private string[] _withVariables; + + public JtlWriter(string directory, string fileName) + { + _directory = directory; + _fileName = fileName; + } + + /// + /// Allows setting to include all fields in XML format. + ///
+ /// This is just a shorter way of using with true setting. + ///
+ /// the JtlWriter for further configuration or usage. + /// + public JtlWriter WithAllFields() => + WithAllFields(true); + + /// + /// Allows setting if all or none fields are enabled when saving the JTL. + ///
+ /// If you enable them all, then XML format will be used. + ///
+ /// Take into consideration that having a JTL writer with no fields enabled makes no sense. But, + /// you may want to disable all fields to then enable specific ones, and not having to manually + /// disable each of default included fields manually. The same applies when you want most of the + /// fields except for some: in such case you can enable all and then manually disable the ones that + /// you want to exclude. + ///
+ /// Also take into consideration that the more fields you add to JTL writer, the more time JMeter + /// will spend on saving the information, and the more disk the file will consume. So, include + /// fields thoughtfully. + ///
+ /// specifies whether enable or disable all fields. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithAllFields(bool enabled) + { + _withAllFields = enabled; + return this; + } + + /// + /// Allows specifying to use XML or CSV format for saving JTL. + ///
+ /// Take into consideration that some fields (like requestHeaders, responseHeaders, etc.) will only + /// be saved when XML format is used. + ///
+ /// specifies whether enable XML format saving, or disable it (and use CSV). By + /// default, it is set to false. + /// the JtlWriter for further configuration or usage. + public JtlWriter SaveAsXml(bool enabled) + { + _saveAsXml = enabled; + return this; + } + + /// + /// Allows setting whether or not to include elapsed time (milliseconds spent in each sample) in + /// generated JTL. + ///
+ /// This is usually the most important metric to collect during a performance test, so in general + /// this should be included. + ///
+ /// specifies whether enable or disable inclusion of elapsed time. By default, it is + /// set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithElapsedTime(bool enabled) + { + _withElapsedTime = enabled; + return this; + } + + /// + /// Allows setting whether or not to include response message (eg: "OK" for HTTP 200 status code) + /// in generated JTL. + ///
+ /// This property is usually handy to trace potential issues, specially the ones that are not + /// standard issues (like HTTPConnectionExceptions) which are not deducible from response code. + ///
+ /// specifies whether enable or disable inclusion of response message. By default, + /// it is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithResponseMessage(bool enabled) + { + _withResponseMessage = enabled; + return this; + } + + /// + /// Allows setting whether or not to include success (a bool indicating if request was success + /// or not) field in generated JTL. + ///
+ /// This property is usually handy to easily identify if a request failed or not (either due to + /// default JMeter logic, or due to some assertion check or post processor alteration). + ///
+ /// specifies whether enable or disable inclusion of success field. By default, it + /// is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithSuccess(bool enabled) + { + _withSuccess = enabled; + return this; + } + + /// + /// Allows setting whether or not to include sent bytes count (number of bytes sent to server by + /// request) field in generated JTL. + ///
+ /// This property is helpful when requests are dynamically generated or when you want to easily + /// evaluate how much data/load has been transferred to the server. + ///
+ /// specifies whether enable or disable inclusion of sent bytes count. By default, + /// it is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithSentByteCount(bool enabled) + { + _withSentByteCount = enabled; + return this; + } + + /// + /// Allows setting whether or not to include response file name (name of file stored by + /// ) field in generated JTL. + ///
+ /// This property is helpful when ResponseFileSaver is used to easily trace the request response + /// contents and don't have to include them in JTL file itself. + ///
+ /// specifies whether enable or disable inclusion of response file name. By default, + /// it is set to false. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithResponseFilename(bool enabled) + { + _withResponseFilename = enabled; + return this; + } + + /// + /// Allows setting whether or not to include the response encoding (eg: UTF-8, ISO-8859-1, etc.) + /// field in generated JTL. + /// + /// specifies whether enable or disable inclusion of response encoding. By default, + /// it is set to false. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithEncoding(bool enabled) + { + _withEncoding = enabled; + return this; + } + + /// + /// Allows setting whether or not to include the Idle time (milliseconds spent in JMeter + /// processing, but not sampling, generally 0) field in generated JTL. + /// + /// specifies whether enable or disable inclusion of idle time. By default, it is + /// set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithIdleTime(bool enabled) + { + _withIdleTime = enabled; + return this; + } + + /// + /// Allows setting whether or not to include response headers (eg: HTTP headers like Content-Type + /// and the like) field in generated JTL. + ///
+ /// Note: this field will only be saved if is also set to + /// true. + ///
+ /// specifies whether enable or disable inclusion of response headers. By default, + /// it is set to false. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithResponseHeaders(bool enabled) + { + _withResponseHeaders = enabled; + return this; + } + + /// + /// Allows setting whether or not to include assertion results (with name, success field, and + /// potential error message) info in generated JTL. + ///
+ /// Note: this will only be saved if is also set to + /// true. + ///
+ /// This info is handy when tracing why requests are marked as failure and exact reason. + ///
+ /// specifies whether enable or disable inclusion of assertion results. By default, + /// it is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithAssertionResults(bool enabled) + { + _withAssertionResults = enabled; + return this; + } + + /// + /// Allows setting whether or not to include assertion results (with name, success field, and + /// potential error message) info in generated JTL. + ///
+ /// Note: this will only be saved if is set to false (or not + /// set, which defaults XML save to false). + ///
+ /// specifies whether enable or disable inclusion of assertion results. By default, + /// it is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithFieldNames(bool enabled) + { + _withFieldNames = enabled; + return this; + } + + /// + /// Allows setting whether or not to include sample label (i.e.: name of the request) field in + /// generated JTL. + ///
+ /// In general, you should enable this field to properly identify results to associated samplers. + ///
+ /// specifies whether enable or disable inclusion of sample labels. By default, it + /// is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithLabel(bool enabled) + { + _withLabel = enabled; + return this; + } + + /// + /// Allows setting whether or not to include thread name field in generated JTL. + ///
+ /// This is helpful to identify the requests generated by each thread and allow tracing + /// "correlated" requests (requests that are associated to previous requests in same thread). + ///
+ /// specifies whether enable or disable inclusion of thread name. By default, it is + /// set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithThreadName(bool enabled) + { + _withThreadName = enabled; + return this; + } + + /// + /// Allows setting whether or not to include assertion failure message field in generated JTL. + ///
+ /// This is helpful to trace potential reason of a request being marked as failure. + ///
+ /// specifies whether enable or disable inclusion of assertion failure message. By + /// default, it is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithAssertionFailureMessage(bool enabled) + { + _withAssertionFailureMessage = enabled; + return this; + } + + /// + /// Allows setting whether or not to include active thread counts (basically, number of concurrent + /// requests, both in the sample thread group, and in all thread groups) fields in generated JTL. + ///
+ /// This is helpful to know under how much load (concurrent requests) is the tested service at the + /// moment the request was done. + ///
+ /// specifies whether enable or disable inclusion of active thread counts. By + /// default, it is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithActiveThreadCounts(bool enabled) + { + _withActiveThreadCounts = enabled; + return this; + } + + /// + /// Allows setting whether or not to include latency time (milliseconds between the sample started + /// and first byte of response is received) field in generated JTL. + ///
+ /// This is usually helpful to identify how fast does the tested service takes to answer, taking + /// out the time spent in transferring response data. + ///
+ /// specifies whether enable or disable inclusion of latency time. By default, it is + /// set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithLatency(bool enabled) + { + _withLatency = enabled; + return this; + } + + /// + /// Allows setting whether or not to include sample counts (total and error counts) fields in + /// generated JTL. + ///
+ /// In general sample count will be 1, and error count will be 0 or 1 depending on sample success + /// or failure. But there are some scenarios where these counts might be greater, for example when + /// controllers results are being included. + ///
+ /// specifies whether enable or disable inclusion of sample counts. By default, it + /// is set to false. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithSampleAndErrorCounts(bool enabled) + { + _withSampleAndErrorCounts = enabled; + return this; + } + + /// + /// Allows setting whether or not to include request headers (eg: HTTP headers like User-Agent and + /// the like) field in generated JTL. + ///
+ /// Note: this field will only be saved if is also set to + /// true. + ///
+ /// specifies whether enable or disable inclusion of request headers. By default, it + /// is set to false. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithRequestHeaders(bool enabled) + { + _withRequestHeaders = enabled; + return this; + } + + /// + /// Allows setting whether or not to include response body field in generated JTL. + ///
+ /// Note: this field will only be saved if is also set to + /// true. + ///
+ /// This is usually helpful for tracing the response obtained by each sample. Consider using + /// to get a file for each response body. + ///
+ /// specifies whether enable or disable inclusion of response body. By default, it + /// is set to false. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithResponseData(bool enabled) + { + _withResponseData = enabled; + return this; + } + + /// + /// Allows setting whether or not to include timestamp (epoch when the sample started) field in + /// generated JTL. + /// + /// specifies whether enable or disable inclusion of timestamps. By default, it is + /// set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithTimeStamp(bool enabled) + { + _withTimeStamp = enabled; + return this; + } + + /// + /// Allows setting whether or not to include response codes (e.g.: 200) field in generated JTL. + ///
+ /// This field allows to quickly identify different reasons for failure in server (eg: bad request, + /// service temporally unavailable, etc.). + ///
+ /// specifies whether enable or disable inclusion of response codes. By default, it + /// is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithResponseCode(bool enabled) + { + _withResponseCode = enabled; + return this; + } + + /// + /// Allows setting whether or not to include response data type (i.e.: binary or text) field in + /// generated JTL. + /// + /// specifies whether enable or disable inclusion of response data types. By + /// default, it is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithDataType(bool enabled) + { + _withDataType = enabled; + return this; + } + + /// + /// Allows setting whether or not to include received bytes count (number of bytes sent by server + /// in the response) field in generated JTL. + ///
+ /// This property is helpful to measure how much load is the network getting and how much + /// information is the tested service generating. + ///
+ /// specifies whether enable or disable inclusion of received bytes counts. By + /// default, it is set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithReceivedByteCount(bool enabled) + { + _withReceivedByteCount = enabled; + return this; + } + + /// + /// Allows setting whether or not to include url field in generated JTL. + ///
+ /// This property is helpful when URLs are dynamically generated and may vary for the sample + /// sampler + ///
+ /// specifies whether enable or disable inclusion of urls. By default, it is set to + /// true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithUrl(bool enabled) + { + _withUrl = enabled; + return this; + } + + /// + /// Allows setting whether or not to include connect time (milliseconds between the sample started + /// and connection is established to service to start sending request) field in generated JTL. + ///
+ /// This is usually helpful to identify issues in network latency when connecting or server load + /// when serving connection requests. + ///
+ /// specifies whether enable or disable inclusion of connect time. By default, it is + /// set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithConnectTime(bool enabled) + { + _withConnectTime = enabled; + return this; + } + + /// + /// Allows setting whether or not to include host name (name of host that did the sample) field in + /// generated JTL. + ///
+ /// This particularly helpful when running JMeter in a distributed fashion to identify which node + /// the sample result is associated to. + ///
+ /// specifies whether enable or disable inclusion of host names. By default, it is + /// set to false. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithHostname(bool enabled) + { + _withHostname = enabled; + return this; + } + + /// + /// Allows setting whether or not to include sampler data (like cookies, HTTP method, request body + /// and redirection URL) entries in generated JTL. + ///
+ /// Note: this field will only be saved if is also set to + /// true. + ///
+ /// specifies whether enable or disable inclusion of sample data. By default, it is + /// set to false. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithSamplerData(bool enabled) + { + _withSamplerData = enabled; + return this; + } + + /// + /// Allows setting whether or not to include sub results (like redirects) entries in generated + /// JTL. + /// + /// specifies whether enable or disable inclusion of sub results. By default, it is + /// set to true. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithSubResults(bool enabled) + { + _withSubResults = enabled; + return this; + } + + /// + /// Allows specifying JMeter variables to include in generated jtl file. + ///
+ /// Warning: variables to sample are test plan wide. This means that if you set them in one + /// jtl writer, they will appear in all jtl writers used in the test plan. Moreover, if you set + /// them in different jtl writers, only variables set on latest one will be considered. + ///
+ /// names of JMeter variables to include in jtl file. + /// the JtlWriter for further configuration or usage. + public JtlWriter WithVariables(params string[] variables) + { + _withVariables = variables; + return this; + } + } +} diff --git a/Abstracta.JmeterDsl/Core/Listeners/ResponseFileSaver.cs b/Abstracta.JmeterDsl/Core/Listeners/ResponseFileSaver.cs new file mode 100644 index 0000000..00a7a2e --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Listeners/ResponseFileSaver.cs @@ -0,0 +1,24 @@ +namespace Abstracta.JmeterDsl.Core.Listeners +{ + /// + /// Generates one file for each response of a sample/request. + ///
+ /// This element is dependant of the scope: this means that if you add it at test plan level it will + /// generate files for all samplers in test plan, if added at thread group level then it will + /// generate files for samplers only in the thread group, and if you add it at sampler level it will + /// generate files only for the associated sampler. + ///
+ /// By default, it will generate one file for each response using the given (which might include the + /// directory location) prefix to create the files and adding an incremental number to each response + /// and an extension according to the response mime type. + ///
+ public class ResponseFileSaver : BaseListener + { + private readonly string _fileNamePrefix; + + public ResponseFileSaver(string fileNamePrefix) + { + _fileNamePrefix = fileNamePrefix; + } + } +} diff --git a/Abstracta.JmeterDsl/Core/Listeners/ResultsTreeVisualizer.cs b/Abstracta.JmeterDsl/Core/Listeners/ResultsTreeVisualizer.cs new file mode 100644 index 0000000..3b31db2 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Listeners/ResultsTreeVisualizer.cs @@ -0,0 +1,32 @@ +namespace Abstracta.JmeterDsl.Core.Listeners +{ + /// + /// Shows a popup window including live results tree using JMeter built-in View Results Tree + /// element. + ///
+ /// If resultsTreeVisualizer is added at testPlan level it will show information about all samples in + /// the test plan, if added at thread group level it will only show samples for samplers contained + /// within it, if added as a sampler child, then only that sampler samples will be shown. + ///
+ public class ResultsTreeVisualizer : BaseListener + { + protected int? _resultsLimit; + + /// + /// Specifies the maximum number of sample results to show. + ///
+ /// When the limit is reached, only latest sample results are shown. + ///
+ /// Take into consideration that the greater the number of displayed results, the more system + /// memory is required, which might cause an OutOfMemoryError depending on JVM settings. + ///
+ /// the maximum number of sample results to show. When not set the default + /// value is 500. + /// the visualizer for further configuration or usage. + public ResultsTreeVisualizer ResultsLimit(int resultsLimit) + { + _resultsLimit = resultsLimit; + return this; + } + } +} diff --git a/Abstracta.JmeterDsl/Core/Samplers/BaseSampler.cs b/Abstracta.JmeterDsl/Core/Samplers/BaseSampler.cs new file mode 100644 index 0000000..4664f46 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Samplers/BaseSampler.cs @@ -0,0 +1,20 @@ +using System; +using Abstracta.JmeterDsl.Core.TestElements; +using Abstracta.JmeterDsl.Core.ThreadGroups; + +namespace Abstracta.JmeterDsl.Core.Samplers +{ + /// + /// Hosts common logic to all samplers. + ///
+ /// In particular, it specifies that samplers are and containing . + /// For an example of an implementation of a sampler check + ///
+ public abstract class BaseSampler : TestElementContainer, IThreadGroupChild + { + protected BaseSampler(string name) + : base(name, Array.Empty()) + { + } + } +} diff --git a/Abstracta.JmeterDsl/Core/Samplers/DslDummySampler.cs b/Abstracta.JmeterDsl/Core/Samplers/DslDummySampler.cs new file mode 100644 index 0000000..00534d6 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Samplers/DslDummySampler.cs @@ -0,0 +1,143 @@ +using System; + +namespace Abstracta.JmeterDsl.Core.Samplers +{ + /// + /// Allows using JMeter Dummy Sampler plugin to emulate other samples and ease testing post + /// processors and other parts of a test plan. + ///
+ /// By default, this element is set with no request, url, response code=200, response message = OK, + /// and response time with random value between 50 and 500 milliseconds. Additionally, emulation of + /// response times (through sleeps) is disabled to speed up testing. + ///
+ public class DslDummySampler : BaseSampler + { + private readonly string _responseBody; + private bool? _successful; + private string _responseCode; + private string _responseMessage; + private string _responseTime; + private bool? _simulateResponseTime; + private string _url; + private string _requestBody; + + public DslDummySampler(string name, string responseBody) + : base(name) + { + _responseBody = responseBody; + } + + /// + /// Allows generating successful or unsuccessful sample results for this sampler. + /// + /// when true, generated sample result will be successful, otherwise it will be + /// marked as failure. When not specified, successful sample results are + /// generated. + /// the sampler for further configuration or usage. + public DslDummySampler Successful(bool successful) + { + _successful = successful; + return this; + } + + /// + /// Specifies the response code included in generated sample results. + /// + /// defines the response code included in sample results. When not set, 200 is used. + /// the sampler for further configuration or usage. + public DslDummySampler ResponseCode(string code) + { + _responseCode = code; + return this; + } + + /// + /// Specifies the response message included in generated sample results. + /// + /// defines the response message included in sample results. When not set, OK is + /// used. + /// the sampler for further configuration or usage. + public DslDummySampler ResponseMessage(string message) + { + _responseMessage = message; + return this; + } + + /// + /// Specifies the response time used in generated sample results. + /// + /// defines the response time associated to the sample results. When not set, a + /// randomly calculated value between 50 and 500 milliseconds is used. + /// the sampler for further configuration or usage. + public DslDummySampler ResponseTime(TimeSpan responseTime) + { + _responseTime = ((long)responseTime.TotalMilliseconds).ToString(); + return this; + } + + /// + /// Same as but allowing to specify a JMeter expression for + /// evaluation. + ///
+ /// This is useful when you want response time to be calculated dynamically. For example, + /// ${__Random(50, 500)}} + ///
+ /// specifies the JMeter expression to be used to calculate response times, + /// in milliseconds, for the sampler. + /// the sampler for further configuration or usage. + /// + public DslDummySampler ResponseTime(string responseTime) + { + _responseTime = responseTime; + return this; + } + + /// + /// Specifies if used response time should be simulated (the sample will sleep for the given + /// duration) or not. + ///
+ /// Having simulation disabled allows for really fast emulation and trial of test plan, which is + /// very handy when debugging. If you need a more accurate emulation in more advanced cases, like + /// you don't want to generate too many requests per second, and you want a behavior closer to the + /// real thing, then consider enabling response time simulation. + ///
+ /// when true enables simulation of response times, when false no wait is done + /// speeding up test plan execution. By default, simulation is disabled. + /// the sampler for further configuration or usage. + public DslDummySampler SimulateResponseTime(bool simulate) + { + _simulateResponseTime = simulate; + return this; + } + + /// + /// Specifies the URL used in generated sample results. + ///
+ /// This might be helpful in scenarios where extractors, pre-processors or other test plan elements + /// depend on the URL. + ///
+ /// defines the URL associated to generated sample results. When not set, an empty URL + /// is used. + /// the sampler for further configuration or usage. + public DslDummySampler Url(string url) + { + _url = url; + return this; + } + + /// + /// Specifies the request body used in generated sample results. + ///
+ /// This might be helpful in scenarios where extractors, pre-processors or other test plan elements + /// depend on the request body. + ///
+ /// defines the request body associated to generated sample results. When not + /// set, an empty body is used. + /// the sampler for further configuration or usage. + public DslDummySampler RequestBody(string requestBody) + { + _requestBody = requestBody; + return this; + } + } +} diff --git a/Abstracta.JmeterDsl/Core/Samplers/ISamplerChild.cs b/Abstracta.JmeterDsl/Core/Samplers/ISamplerChild.cs new file mode 100644 index 0000000..60afa69 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Samplers/ISamplerChild.cs @@ -0,0 +1,9 @@ +namespace Abstracta.JmeterDsl.Core.Samplers +{ + /// + /// Test elements which can be nested as children of a sampler in JMeter, should implement this interface. + /// + public interface ISamplerChild : IDslTestElement + { + } +} diff --git a/Abstracta.JmeterDsl/Core/Stats/CountMetricSummary.cs b/Abstracta.JmeterDsl/Core/Stats/CountMetricSummary.cs new file mode 100644 index 0000000..f37d75d --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Stats/CountMetricSummary.cs @@ -0,0 +1,18 @@ +namespace Abstracta.JmeterDsl.Core.Stats +{ + /// + /// Provides summary data for a set of count values. + /// + public class CountMetricSummary + { + /// + /// Provides the total count (the sum). + /// + public long Total { get; set; } + + /// + /// Provides the average count per second for the given metric. + /// + public double PerSecond { get; set; } + } +} diff --git a/Abstracta.JmeterDsl/Core/Stats/StatsSummary.cs b/Abstracta.JmeterDsl/Core/Stats/StatsSummary.cs new file mode 100644 index 0000000..50259be --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Stats/StatsSummary.cs @@ -0,0 +1,71 @@ +using System; + +namespace Abstracta.JmeterDsl.Core.Stats +{ + /// + /// Contains summary statistics of a group of collected sample results. + /// + public class StatsSummary + { + /// + /// Gets the instant when the first sample started. + ///
+ /// When associated to a test plan or transaction it gets its start time. + ///
+ public DateTime FirstTime { get; set; } + + /// + /// Gets the instant when the last sample ended. + ///
+ /// When associated to a test plan or transaction it gets its end time. + ///
+ /// Take into consideration that for transactions this time takes not only into consideration the + /// endTime of last sample, but also the time spent in timers and pre and postprocessors. + ///
+ public DateTime EndTime { get; set; } + + /// + /// Gets metrics for number of samples + ///
+ /// This counts both failing and passing samples. + ///
+ public CountMetricSummary Samples { get; set; } + + /// + /// Gets the total number of samples. + /// + public long SamplesCount => Samples.Total; + + /// + /// Gets metrics for number of samples that failed. + /// + public CountMetricSummary Errors { get; set; } + + /// + /// Gets the total number of samples that failed. + /// + public long ErrorsCount => Errors.Total; + + /// + /// Gets metrics for time spent in samples. + /// + public TimeMetricSummary SampleTime { get; set; } + + /// + /// Gets the 99 percentile of samples times. + ///
+ /// 99% of samples took less or equal to the returned value. + ///
+ public TimeSpan SampleTimePercentile99 => SampleTime.Perc99; + + /// + /// Gets metrics for received bytes in sample responses. + /// + public CountMetricSummary ReceivedBytes { get; set; } + + /// + /// Gets metrics for sent bytes in samples requests. + /// + public CountMetricSummary SentBytes { get; set; } + } +} diff --git a/Abstracta.JmeterDsl/Core/Stats/TimeMetricSummary.cs b/Abstracta.JmeterDsl/Core/Stats/TimeMetricSummary.cs new file mode 100644 index 0000000..1098d14 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Stats/TimeMetricSummary.cs @@ -0,0 +1,55 @@ +using System; + +namespace Abstracta.JmeterDsl.Core.Stats +{ + /// + /// Provides summary data for a set of timing values. + /// + public class TimeMetricSummary + { + /// + /// Gets the minimum collected value. + /// + public TimeSpan Min { get; set; } + + /// + /// Gets the maximum collected value. + /// + public TimeSpan Max { get; set; } + + /// + /// Gets the mean/average of collected values. + /// + public TimeSpan Mean { get; set; } + + /// + /// Gets the median of collected values. + ///
+ /// The median is the same as percentile 50, and is the value for which 50% of the collected values + /// is smaller/greater. + /// This value might differ from when distribution of values is not symmetric. + ///
+ public TimeSpan Median { get; set; } + + /// + /// Gets the 90 percentile of samples times. + ///
+ /// 90% of samples took less or equal to the returned value. + ///
+ public TimeSpan Perc90 { get; set; } + + /// + /// Gets the 95 percentile of samples times. + ///
+ /// 95% of samples took less or equal to the returned value. + ///
+ public TimeSpan Perc95 { get; set; } + + /// + /// Gets the 99 percentile of samples times. + ///
+ /// 99% of samples took less or equal to the returned value. + ///
+ public TimeSpan Perc99 { get; set; } + } +} diff --git a/Abstracta.JmeterDsl/Core/TestElements/BaseTestElement.cs b/Abstracta.JmeterDsl/Core/TestElements/BaseTestElement.cs new file mode 100644 index 0000000..5a3974b --- /dev/null +++ b/Abstracta.JmeterDsl/Core/TestElements/BaseTestElement.cs @@ -0,0 +1,20 @@ +using Abstracta.JmeterDsl.Core.Bridge; + +namespace Abstracta.JmeterDsl.Core.TestElements +{ + /// + /// Provides the basic logic for all . + /// + public abstract class BaseTestElement : IDslTestElement + { + private readonly string _name; + + public BaseTestElement(string name) + { + _name = name; + } + + public void ShowInGui() => + new BridgeService().ShowTestElementInGui(this); + } +} diff --git a/Abstracta.JmeterDsl/Core/TestElements/IMultiLevelTestElement.cs b/Abstracta.JmeterDsl/Core/TestElements/IMultiLevelTestElement.cs new file mode 100644 index 0000000..8205a44 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/TestElements/IMultiLevelTestElement.cs @@ -0,0 +1,13 @@ +using Abstracta.JmeterDsl.Core.Samplers; +using Abstracta.JmeterDsl.Core.ThreadGroups; + +namespace Abstracta.JmeterDsl.Core.TestElements +{ + /// + /// This is just a simple interface to avoid code duplication for test elements that apply at + /// different levels of a test plan(at test plan, thread group or as sampler child). + /// + public interface IMultiLevelTestElement : ITestPlanChild, IThreadGroupChild, ISamplerChild + { + } +} diff --git a/Abstracta.JmeterDsl/Core/TestElements/TestElementContainer.cs b/Abstracta.JmeterDsl/Core/TestElements/TestElementContainer.cs new file mode 100644 index 0000000..006856a --- /dev/null +++ b/Abstracta.JmeterDsl/Core/TestElements/TestElementContainer.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Abstracta.JmeterDsl.Core.TestElements +{ + /// + /// Abstracts logic for that can nest other test elements. + /// + /// is type of test elements that can be nested by this class. + public abstract class TestElementContainer : BaseTestElement + { + protected List _children = new List(); + + protected TestElementContainer(string name, C[] children) + : base(name) + { + _children = children.ToList(); + } + } +} diff --git a/Abstracta.JmeterDsl/Core/TestPlanStats.cs b/Abstracta.JmeterDsl/Core/TestPlanStats.cs new file mode 100644 index 0000000..366f2bb --- /dev/null +++ b/Abstracta.JmeterDsl/Core/TestPlanStats.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using Abstracta.JmeterDsl.Core.Stats; + +namespace Abstracta.JmeterDsl.Core +{ + /// + /// Contains all statistics collected during the execution of a test plan. + ///
+ /// When using different samples, specify different names on them to be able to get each sampler + /// specific statistics after they run. + ///
+ public class TestPlanStats + { + /// + /// Provides the time taken to run the test plan. + /// + public TimeSpan Duration { get; set; } + + /// + /// Provides statistics for the entire test plan. + /// + public StatsSummary Overall { get; set; } + + /// + /// Provides statistics for each label (usually, samplers labels). + /// + public Dictionary Labels { get; set; } + } +} diff --git a/Abstracta.JmeterDsl/Core/ThreadGroups/BaseThreadGroup.cs b/Abstracta.JmeterDsl/Core/ThreadGroups/BaseThreadGroup.cs new file mode 100644 index 0000000..b8bde49 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/ThreadGroups/BaseThreadGroup.cs @@ -0,0 +1,15 @@ +using Abstracta.JmeterDsl.Core.TestElements; + +namespace Abstracta.JmeterDsl.Core.ThreadGroups +{ + /// + /// Contains common logic for all Thread Groups. + /// + public abstract class BaseThreadGroup : TestElementContainer, ITestPlanChild + { + protected BaseThreadGroup(string name, IThreadGroupChild[] children) + : base(name, children) + { + } + } +} diff --git a/Abstracta.JmeterDsl/Core/ThreadGroups/DslThreadGroup.cs b/Abstracta.JmeterDsl/Core/ThreadGroups/DslThreadGroup.cs new file mode 100644 index 0000000..4c0b89f --- /dev/null +++ b/Abstracta.JmeterDsl/Core/ThreadGroups/DslThreadGroup.cs @@ -0,0 +1,28 @@ +using System; + +namespace Abstracta.JmeterDsl.Core.ThreadGroups +{ + /// + /// Represents the standard thread group test element included by JMeter. + /// + public class DslThreadGroup : BaseThreadGroup + { + private readonly int? _threads; + private readonly int? _iterations; + private readonly TimeSpan? _duration; + + public DslThreadGroup(string name, int threads, int iterations, IThreadGroupChild[] children) + : base(name, children) + { + _threads = threads; + _iterations = iterations; + } + + public DslThreadGroup(string name, int threads, TimeSpan duration, IThreadGroupChild[] children) + : base(name, children) + { + _threads = threads; + _duration = duration; + } + } +} diff --git a/Abstracta.JmeterDsl/Core/ThreadGroups/IThreadGroupChild.cs b/Abstracta.JmeterDsl/Core/ThreadGroups/IThreadGroupChild.cs new file mode 100644 index 0000000..db14809 --- /dev/null +++ b/Abstracta.JmeterDsl/Core/ThreadGroups/IThreadGroupChild.cs @@ -0,0 +1,9 @@ +namespace Abstracta.JmeterDsl.Core.ThreadGroups +{ + /// + /// Test elements that can be added as direct children of a thread group in jmeter should implement this interface. + /// + public interface IThreadGroupChild : IDslTestElement + { + } +} diff --git a/Abstracta.JmeterDsl/Http/DslHttpCache.cs b/Abstracta.JmeterDsl/Http/DslHttpCache.cs new file mode 100644 index 0000000..067b358 --- /dev/null +++ b/Abstracta.JmeterDsl/Http/DslHttpCache.cs @@ -0,0 +1,33 @@ +using Abstracta.JmeterDsl.Core.Configs; + +namespace Abstracta.JmeterDsl.Http +{ + /// + /// Allows configuring caching behavior used by HTTP samplers. + ///
+ /// This element can only be added as child of test plan, and currently allows only to disable HTTP + /// caching which is enabled by default (emulating browser behavior). + ///
+ /// This element has to be added before any http sampler to be considered, and if you add multiple + /// instances of cache manager to a test plan, only the first one will be considered. + ///
+ public class DslHttpCache : BaseConfigElement + { + protected bool? _disable; + + public DslHttpCache() + : base(null) + { + } + + /// + /// disables HTTP caching for the test plan. + /// + /// the DslHttpCache to allow fluent API usage. + public DslHttpCache Disable() + { + _disable = true; + return this; + } + } +} diff --git a/Abstracta.JmeterDsl/Http/DslHttpCookies.cs b/Abstracta.JmeterDsl/Http/DslHttpCookies.cs new file mode 100644 index 0000000..ed1f61b --- /dev/null +++ b/Abstracta.JmeterDsl/Http/DslHttpCookies.cs @@ -0,0 +1,48 @@ +using Abstracta.JmeterDsl.Core.Configs; + +namespace Abstracta.JmeterDsl.Http +{ + /// + /// Allows configuring cookies settings used by HTTP samplers. + ///
+ /// This element can only be added as child of test plan, and currently allows only to disable HTTP + /// cookies handling which is enabled by default (emulating browser behavior). + ///
+ /// This element has to be added before any http sampler to be considered, and if you add multiple + /// instances of cookie manager to a test plan, only the first one will be considered. + ///
+ public class DslHttpCookies : BaseConfigElement + { + protected bool? _disable; + protected bool? _clearCookiesBetweenIterations; + + public DslHttpCookies() + : base(null) + { + } + + /// + /// Disables HTTP cookies handling for the test plan. + /// + /// the DslHttpCookies to allow fluent API usage. + public DslHttpCookies Disable() + { + _disable = true; + return this; + } + + /// + /// Allows to enable or disable clearing cookies between thread group iterations. + ///
+ /// Cookies are cleared each iteration by default. If this is not desirable, for instance if + /// logging in once and then iterating through actions multiple times, use this to set to false. + ///
+ /// clear boolean to set clearing of cookies. By default, it is set to true. + /// the DslHttpCookies for further configuration or usage. + public DslHttpCookies ClearCookiesBetweenIterations(bool clear) + { + _clearCookiesBetweenIterations = clear; + return this; + } + } +} diff --git a/Abstracta.JmeterDsl/Http/DslHttpSampler.cs b/Abstracta.JmeterDsl/Http/DslHttpSampler.cs new file mode 100644 index 0000000..4b02a6b --- /dev/null +++ b/Abstracta.JmeterDsl/Http/DslHttpSampler.cs @@ -0,0 +1,95 @@ +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using Abstracta.JmeterDsl.Core.Samplers; + +namespace Abstracta.JmeterDsl.Http +{ + /// + /// Allows to configure a JMeter HTTP sampler to make HTTP requests in a test plan. + /// + public class DslHttpSampler : BaseSampler + { + private readonly string _url; + private string _method; + private string _body; + + public DslHttpSampler(string name, string url) + : base(name) + { + _url = url; + } + + /// + /// Specifies that the sampler should send an HTTP POST to defined URL. + /// + /// to include in HTTP POST request body. + /// to be sent as Content-Type header in HTTP POST request. + /// the sampler for further configuration or usage. + public DslHttpSampler Post(string body, MediaTypeHeaderValue contentType) => + Method(HttpMethod.Post.Method) + .ContentType(contentType) + .Body(body); + + /// + /// Specifies the HTTP method to be used in the HTTP request generated by the sampler. + /// + /// is the HTTP method to be used by the sampler. + /// the sampler for further configuration or usage. + public DslHttpSampler Method(string method) + { + _method = method; + return this; + } + + /// + /// Specifies the body to be sent in the HTTP request generated by the sampler. + /// + /// to be used as in the body of the HTTP request. + /// the sampler for further configuration or usage. + public DslHttpSampler Body(string body) + { + _body = body; + return this; + } + + /// + /// Allows to easily specify the Content-Type HTTP header to be used by the sampler. + /// + /// value to send as Content-Type header. + /// the sampler for further configuration or usage. + public DslHttpSampler ContentType(MediaTypeHeaderValue contentType) + { + FindHeaders().ContentType(contentType); + return this; + } + + private HttpHeaders FindHeaders() + { + var ret = (from c in _children + where c is HttpHeaders + select (HttpHeaders)c).FirstOrDefault(); + if (ret == null) + { + ret = new HttpHeaders(); + _children.Add(ret); + } + return ret; + } + + /// + /// Specifies an HTTP header to be sent by the sampler. + ///
+ /// To specify multiple headers just invoke this method several times with the different header + /// names and values. + ///
+ /// specifies name of the HTTP header. + /// specifies value of the HTTP header. + /// the sampler for further configuration or usage. + public DslHttpSampler Header(string name, string value) + { + FindHeaders().Header(name, value); + return this; + } + } +} diff --git a/Abstracta.JmeterDsl/Http/HttpHeaders.cs b/Abstracta.JmeterDsl/Http/HttpHeaders.cs new file mode 100644 index 0000000..1810f97 --- /dev/null +++ b/Abstracta.JmeterDsl/Http/HttpHeaders.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Net.Http.Headers; +using Abstracta.JmeterDsl.Core.TestElements; + +namespace Abstracta.JmeterDsl.Http +{ + /// + /// Allows specifying HTTP headers (through an underlying JMeter HttpHeaderManager) to be used by + /// HTTP samplers. + ///
+ /// This test element can be added at different levels (in the same way as HTTPHeaderManager) of a + /// test plan affecting all samplers in the scope were is added.For example if httpHeaders is + /// specified at test plan, then all headers will apply to http samplers; if it is specified on + /// thread group, then only samplers on that thread group would be affected; if specified as a child + /// of a sampler, only the particular sampler will include such headers.Also take into consideration + /// that headers specified at lower scope will overwrite ones specified at higher scope (eg: sampler + /// child headers will overwrite test plan headers). + ///
+ public class HttpHeaders : BaseTestElement, IMultiLevelTestElement + { + private readonly Dictionary _headers = new Dictionary(); + + public HttpHeaders() + : base(null) + { + } + + /// + /// Allows to set an HTTP header to be used by HTTP samplers. + ///
+ /// To specify multiple headers just invoke this method several times with the different header + /// names and values. + ///
+ /// specifies name of the HTTP header. + /// specifies value of the HTTP header. + /// the config element for further configuration or usage. + public HttpHeaders Header(string name, string value) + { + _headers[name] = value; + return this; + } + + /// + /// Allows to easily specify the Content-Type HTTP header. + /// + /// value to use as Content-Type header. + /// the config element for further configuration or usage. + public HttpHeaders ContentType(MediaTypeHeaderValue value) + { + _headers["Content-Type"] = value.ToString(); + return this; + } + } +} diff --git a/Abstracta.JmeterDsl/JmeterDsl.cs b/Abstracta.JmeterDsl/JmeterDsl.cs new file mode 100644 index 0000000..233d1d3 --- /dev/null +++ b/Abstracta.JmeterDsl/JmeterDsl.cs @@ -0,0 +1,211 @@ +using System; +using Abstracta.JmeterDsl.Core; +using Abstracta.JmeterDsl.Core.Listeners; +using Abstracta.JmeterDsl.Core.Samplers; +using Abstracta.JmeterDsl.Core.ThreadGroups; +using Abstracta.JmeterDsl.Http; + +namespace Abstracta.JmeterDsl +{ + /// + /// This is the main class to be imported from any code using JMeter DSL. + ///
+ /// This class contains factory methods to create DslTestElement instances that allow + /// specifying test plans and associated test elements(samplers, thread groups, listeners, etc.). + /// If you want to support new test elements, then you either add them here (if they are considered + /// to be part of the core of JMeter), or implement another similar class containing only the + /// specifics of the protocol, repository, or grouping of test elements that you want to build + /// (eg, one might implement an Http2JMeterDsl class with only http2 test elements' factory methods). + ///
+ /// When implementing new factory methods, consider adding only the main properties of the test + /// elements as parameters (the ones that make sense to specify in most cases). For the rest of the + /// parameters (the optional ones), prefer specifying them as methods of the implemented + /// DslTestElement for such cases, in a similar fashion as the Builder Pattern. + ///
+ public static class JmeterDsl + { + /// + /// Builds a new test plan. + /// + /// The list of test elements that compose the test plan. + /// The test plan instance. + /// + public static DslTestPlan TestPlan(params ITestPlanChild[] children) => + new DslTestPlan(children); + + /// + /// Builds a new thread group with a given number of threads & iterations. + /// + /// Specifies the number of threads to simulate concurrent virtual users. + /// Specifies the number of iterations that each virtual user will run of children elements until it stops. + /// If you specify -1, then threads will iterate until test plan execution is interrupted (you manually stop the running process, there is an error and thread group is configured to stop on error, or some other explicit termination condition). + /// Setting this property to -1 is in general not advised, since you might inadvertently end up running a test plan without limits consuming unnecessary computing power. Prefer specifying a big value as a safe limit for iterations or duration instead. + /// Contains the test elements that each thread will execute in each iteration. + /// The thread group instance. + /// + public static DslThreadGroup ThreadGroup(int threads, int iterations, params IThreadGroupChild[] children) => + ThreadGroup(null, threads, iterations, children); + + /// + /// Same as but allowing to set a name on the thread group. + ///
+ /// Setting a proper name allows to properly identify the requests generated in each thread group. + ///
+ /// + public static DslThreadGroup ThreadGroup(string name, int threads, int iterations, params IThreadGroupChild[] children) => + new DslThreadGroup(name, threads, iterations, children); + + /// + /// Builds a new thread group with a given number of threads & their duration. + /// + /// to simulate concurrent virtual users. + /// to keep each thread running for this period of time. Take into consideration + /// that JMeter supports specifying duration in seconds, so if you specify a + /// smaller granularity (like milliseconds) it will be rounded up to seconds. + /// contains the test elements that each thread will execute until specified + /// duration is reached. + /// the thread group instance. + /// + public static DslThreadGroup ThreadGroup(int threads, TimeSpan duration, params IThreadGroupChild[] children) => + ThreadGroup(null, threads, duration, children); + + /// + /// Same as but allowing to set a name on the thread group. + ///
+ /// Setting a proper name allows to properly identify the requests generated in each thread group. + ///
+ /// + public static DslThreadGroup ThreadGroup(string name, int threads, TimeSpan duration, params IThreadGroupChild[] children) => + new DslThreadGroup(name, threads, duration, children); + + /// + /// Builds an HTTP Request sampler to sample HTTP requests. + /// + /// Specifies URL the HTTP Request sampler will hit. + /// The HTTP Request sampler instance which can be used to define additional settings for + /// the HTTP request (like method, body, headers, pre & post processors, etc.). + /// + public static DslHttpSampler HttpSampler(string url) => + HttpSampler(null, url); + + /// + /// Same as but allowing to set a name to the HTTP Request sampler. + ///
+ /// Setting a proper name allows to easily identify the requests generated by this sampler and check its particular statistics. + ///
+ /// + public static DslHttpSampler HttpSampler(string name, string url) => + new DslHttpSampler(name, url); + + /// + /// Builds an HTTP header manager which allows setting HTTP headers to be used by HTTPRequest + /// samplers. + /// + /// the HTTP header manager instance which allows specifying the particular HTTP headers to + /// use. + /// + public static HttpHeaders HttpHeaders() => + new HttpHeaders(); + + /// + /// Builds a Cookie manager at the test plan level which allows configuring cookies settings used + /// by HTTPRequest samplers. + /// + /// the http cookies instance which allows configuring cookies settings. + /// + public static DslHttpCookies HttpCookies() => + new DslHttpCookies(); + + /// + /// Builds a Cache manager at the test plan level which allows configuring caching behavior used by + /// HTTPRequest samplers. + /// + /// the http cache instance which allows configuring caching settings. + /// + public static DslHttpCache HttpCache() => + new DslHttpCache(); + + /// + /// Builds a JMeter plugin Dummy Sampler which allows emulating a sampler easing testing other + /// parts of a test plan (like extractors, controllers conditions, etc). + ///
+ /// Usually you would replace an existing sampler with this one, to test some extractor or test + /// plan complex behavior (like controllers conditions), and once you have verified that the rest + /// of the plan works as expected, you place back the original sampler that makes actual + /// interactions to a server. + ///
+ /// By default, this sampler, in contrast to the JMeter plugin Dummy Sampler, does not simulate + /// response time. This helps speeding up the debug and tracing process while using it. + ///
+ /// specifies the response body to be included in generated sample results. + /// the dummy sampler for further configuration and usage in test plan. + /// + public static DslDummySampler DummySampler(string responseBody) + => DummySampler(null, responseBody); + + /// + /// Same as but allowing to set a name on the sampler. + ///
+ /// Setting the name of the sampler allows better simulation the final use case when dummy sampler + /// is replaced by actual/final sampler, when sample results are reported in stats, logs, etc. + ///
+ /// + /// + public static DslDummySampler DummySampler(string name, string responseBody) + => new DslDummySampler(name, responseBody); + + /// + /// Builds a Simple Data Writer to write all collected results to a JTL file. + ///
+ /// This is just a handy short way of generating JTL files using as filename the template: + /// <yyyy-MM-dd HH-mm-ss> <UUID>.jtl + ///
+ /// If you need to have a predictable name, consider using + /// instead. + ///
+ /// specifies the directory path where jtl files will be generated in. If the + /// directory does not exist, then it will be created. + /// the JtlWriter instance + /// + /// + public static JtlWriter JtlWriter(string directory) => + new JtlWriter(directory, null); + + /// + /// Builds a Simple Data Writer to write all collected results to a JTL file. + ///
+ /// This is particularly helpful when you need to control de file name to do later post-processing + /// on the file (eg: use CI build ID in the file name). + ///
+ /// specifies the directory path where jtl file will be generated. If the + /// directory does not exist, then it will be created. + /// the name to be used for the file.File names should be unique, otherwise + /// the new results will be appended to existing file. + /// the JtlWriter instance + public static JtlWriter JtlWriter(string directory, string fileName) => + new JtlWriter(directory, fileName); + + /// + /// Builds a Response File Saver to generate a file for each response of a sample. + /// + /// the prefix to be used when generating the files. This should contain the + /// directory location where the files should be generated and can contain a + /// file name prefix for all file names (eg: target/response-files/response-). + /// the ResponseFileSaver instance. + /// + public static ResponseFileSaver ResponseFileSaver(string fileNamePrefix) => + new ResponseFileSaver(fileNamePrefix); + + /// + /// Builds a View Results Tree element to show live results in a pop-up window while the test + /// runs. + ///
+ /// This element is helpful when debugging a test plan to verify each sample result, and general + /// structure of results. + ///
+ /// the View Results Tree element. + /// + public static ResultsTreeVisualizer ResultsTreeVisualizer() => + new ResultsTreeVisualizer(); + } +} diff --git a/Abstracta.JmeterDsl/pom.xml b/Abstracta.JmeterDsl/pom.xml new file mode 100644 index 0000000..36c0c7e --- /dev/null +++ b/Abstracta.JmeterDsl/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + us.abstracta.jmeter.dotnet + jmeter-dotnet-dsl-parent + 1.0-SNAPSHOT + ../pom.xml + + jmeter-dotnet-dsl + pom + + This pom is only needed to be able to copy jmeter-java-dsl-bridge jar and its dependencies with dependency:copy-dependencies maven plugin goal + + + + us.abstracta.jmeter + jmeter-java-dsl-bridge + ${jmeter-java-dsl.version} + + + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..51b6fc1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,89 @@ +# Contributing + +This guide will introduce you to the code, to help you understand better how it works and send pull requests to submit contributions. + +Before continuing, if you are not familiar with JMeter, please review [JMeter test plan elements](https://jmeter.apache.org/usermanual/test_plan.html) for a basic understanding of JMeter core concepts. + +Also, take into consideration that .Net DSL is heavily based on [Java DSL](https://abstracta.github.io/jmeter-java-dsl). So having at least a basic understanding of [JMeter Java DSL contribution guidelines](https://github.com/abstracta/jmeter-java-dsl/blob/master/CONTRIBUTING.md) is recommended. + +Let's start looking at each of the main classes, and later on, show a diagram to get an overview of their relations. + +## Core classes + +[JmeterDsl](Abstracta.JmeterDsl/JmeterDsl.cs) is the main entry point of the library and provides factory methods that allow the creation and execution of test plans. Each factory method receives as parameters the main attributes of a test element, which are required in most cases, and, when it is a natural container of other test elements (i.e.: it's of no use when no included children), the list of children test elements to nest on it (eg: thread groups to put on a test plan). + +Test elements are classes that implement the [IDslTestElement](Abstracta.JmeterDsl/Core/IDslTestElement.cs) interface, which set up JMeter test elements to be included in a test plan. They might also provide fluent API methods to set optional test element attributes (eg: HTTP method for HTTP sampler) or add children to them. + +.Net DSL additionally provides mostly the same hierarchy of abstract classes and interfaces that JMeter Java DSL provides, which eases the creation of the different test elements that may compose a test plan. + +In general, .Net DSL follows the same structure and rules as Java DSL, but the .Net DSL library is quite different in how it internally works, and in fact, this is caused by it just being a thin .Net adapter of Jmeter Java DSL. + +## .Net DSL internals + +As for you to better understand how .Net DSL works, and its dependency on Jmeter Java DSL, let's briefly review .Net DSL internal logic. + +The .Net DSL library runs a test plan by serializing into an intermediary format (YAML) every element in a test plan. Then, uses the JMeter Java DSL Bridge module to run the serialized test plan. Every time a test plan requires execution, the .Net library invokes Java, providing all required jars (which are embedded resources of the .Net package) as classpath of the JVM, and invoking the BridgeService main class with the YAML test plan sent through the Java process std input. While the test plan runs, all java process std output and std error are redirected to the std output and error of the .Net library executing process. Once execution completes, the java process executing BridgeService generates an output YAML file, which the .Net library uses to provide collected statistics to performance test code. + +The important thing to understand here is that every test element class in the .Net library is just a class that follows certain conventions to serialize it to YAML and then deserialize it in the Java process for test plan execution. + +To get an idea of general test element hierarchy and DSL classes, you might check the [Java DSL class diagram](https://github.com/abstracta/jmeter-java-dsl/blob/master/CONTRIBUTING.md#class-diagram). + +## .Net DSL classes conventions + +Here are the main rules when defining a test element class in the .Net DSL: +* Namespace must match the Java package, but follow .Net conventions. For example: `us.abstracta.jmeter.javadsl.core.samplers` in Java is assumed to be `Abstracta.JmeterDsl.Core.Samplers` in .Net. +* Class name must match the name of the Java builder method (with a potential `Dsl` prefix), or the class name if there is no builder method. Eg: `DslDefaultThreadGroup` in Java is actually `DslThreadGroup` (matching `threadGroup` Java builder method) in .Net. `AzureEngine` in Java is `AzureEngine` in .Net as well (there is no builder method associated to the engine). Even though `AzureEngine` is not actually a test element, since is not part of a test plan, and is in fact a JMeter DSL engine, in general, engine classes apply the same rules as test elements in .Net library. +* Class extends base classes and interfaces that match Java DSL equivalent base classes and interfaces. Eg: `DslHttpSampler` extends `BaseSampler` as in Java code. +* Class declares a protected field (with underscore prefixing) for each builder method parameter and optional test element properties method. Eg: `DslHttpSampler` declares fields `_url` (builder method), `_method` (optional property method), `_body` (optional property method), and inherits `_name` (used in builder methods) from `BaseTestElement`, and `_children` from `TestElementContainer`. +* Class declares constructor with required properties (same as Java DSL). +* Class declares optional properties methods that allow setting optional properties and return an instance of the test element for fluent API usage (same as Java DSL). Eg: `DslHttpSampler` declares `Method` and `Body` methods. +* Class declares additional optional property methods which are just abstractions and simplifications for setting some test element properties. Eg: `DslHttpSampler` declares `Post`, `Header`, and `ContentType` methods. `Post` is just a simplification that actually uses `Method`, `ContentType`, and `Body` methods. `Header` simplifies setting children elements. `ContentType` is a simplified way of using the `Header` method. +* Include xmldoc documentation which contains most of the already contained documentation in the Java docs analogous class, with potential clarifications for the .Net ecosystem. +* Include builder methods in the `JmeterDsl` class to ease the creation of test elements and require the user to just import one namespace and class (`JmeterDsl`). +* Include the test element in a package that is analogous to the Jmeter Java DSL modules. Eg: `DslHttpSampler` is included in `Abstracta.JmeterDsl` as is in Java in `jmeter-java-dsl`. `AzureEngine` is included in `Abstracta.JmeterDsl.Azure` as is in Java in `jmeter-java-dsl-azure`. + +In general, when implementing a new element, it is advisable to explore .Net DSL code, check existing implemented elements that might be similar, and follow the same conventions. + +## Test runs + +As in Java DSL, when you want to run a test plan, it needs to run in a JMeter engine. By default, DslTestPlan uses [EmbeddedJmeterEngine](Abstracta.JmeterDsl/Core/Engines/EmbeddedJmeterEngine.cs), which is the fastest and easiest way to run a test plan, but you might use EmbeddedJmeterEngine as an example and implement your custom engine (for example to run tests in some cloud provider like Azure Load Testing). + +When a test plan runs, the engine returns an instance of [TestPlanStats](Abstracta.JmeterDsl/Core/TestPlanStats.cs), grouping information by test element name (aka label). This allows users to check the expected statistics and verify that everything worked within expected boundaries. + +## Implementing a new DSL test element or feature + +Here we will detail the main steps and things to take into consideration when implementing a new test element, or extending an existing one. + +1. Check if you want something that is already supported by Java DSL and JMeter itself. + * This is very important and implementing support in the .Net library for something that is already supported in Java DSL is super simple. But, implementing something that has not yet support in the JMeter Java DSL, would require you to first contribute it to Java DSL, and then to the .Net library. Implementing something that is not even supported by JMeter, would require probably even more work, but don't despair, and always ask for help or support! + * If you need something that is already supported by Java DSL, follow previously mentioned conventions and continue reading :). + * If you need something that is not supported by Java DSL, then follow [Java DSL guidelines](https://github.com/abstracta/jmeter-java-dsl/blob/master/CONTRIBUTING.md#implementing-a-new-dsl-test-element-or-feature) and, after contributing the changes to Java DSL, follow previously mentioned conventions and continue reading!. +2. Implement tests that verify the expected behavior of the test element in a test plan execution. This way, you verify that you properly initialize JMeter properties and that your interpretation of the test element properties and behavior is right. + * Check [DslHttpSamplerTest](Abstracta.JmeterDsl.Tests/Http/DslHttpSamplerTest.cs) for some sample test cases. +3. Run `dotnet build` and `dotnet test` and fix any potential code styling issues or failing tests. +4. Add a new section [user guide](docs/guide), by adding a new md file and proper `` in parent section, describing the new feature. Consider running in the `docs` directory `pnpm install` and `pnpm dev` (this requires node 18+ and pnpm installed on your machine) to run a local server for docs, where you can review that new changes are properly showing. +5. Commit changes to git, using as a comment a subject line that describes general changes, and if necessary, some additional details describing the reason why the change is necessary. +6. Submit a pull request to the repository including a meaningful name. +7. Check GitHub Actions execution to verify that no test fails on the CI pipeline. +8. When the PR is merged and release is triggered. Enjoy the pleasure and the pride of contributing with an OSS tool :). + +## General coding guidelines + +* Review existing code, and get a general idea of main classes & conventions. It is important to try to keep code consistent to ease maintenance. +* In general, avoid code duplication to ease maintenance and readability. +* Use meaningful names for variables, methods, classes, etc. Avoid acronyms unless they are super intuitive. +* Strive for simplicity and reduce code and complexity whenever possible. Avoid over-engineering (implementing things for potential future scenarios). +* Use comments to describe the reason for some code (the "why"), either because is not the natural/obvious expected code, or because additional clarification is needed. Do not describe the "what". You can use variables, methods & class names to describe the "what". +* Provide xmldoc documentation for all public classes and methods that help users understand when to use the method, test element, etc. +* Avoid leaving `TODO` comments in the code. Create an issue in the GitHub repository, discussion, or include some comment in PR instead. +* Don't leave dead code (commented-out code). +* Avoid including backward incompatible changes (unless required), that would require users to change existing code where they use the API. +* Be gentle and thoughtful when you review code, contribute and submit pull requests :). + +## FAQ + +### I don't understand and still don't know how to implement what I need. What can I do? + +Just create an issue in the repository stating what you need and why, and we will do our best to implement what you need :). + +Or, check existing code. It contains embedded documentation with additional details, and the code never lies. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5001d1 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +![logo](/docs/.vuepress/public/logo.svg) + +Simple .Net API to run performance tests, using [JMeter](http://jmeter.apache.org/) as engine, in a Git and programmers friendly way. + +If you like this project, **please give it a star :star:!** This helps the project be more visible, gain relevance, and encourage us to invest more effort in new features. + +[Here](https://abstracta.github.io/jmeter-java-dsl), you can find the Java DSL. + +Please join [discord server](https://discord.gg/WNSn5hqmSd) or create GitHub [issues](https://github.com/abstracta/jmeter-dotnet-dsl/issues) and [discussions](https://github.com/abstracta/jmeter-dotnet-dsl/discussions) to be part of the community and clear out doubts, get the latest news, propose ideas, report issues, etc. + +## Usage + +Add the package to your project: + +```powershell +dotnet add package Abstracta.JmeterDsl --version 0.1 +``` + +Here is a simple example test using [Nunit](https://nunit.org/)+ with 2 threads/users iterating 10 times each to send HTTP POST requests with a JSON body to `http://my.service`: + +```cs +using System.Net.Http.Headers; +using System.Net.Mime; +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + var stats = TestPlan( + ThreadGroup(2, 10, + HttpSampler("http://my.service") + .Post("{\"name\": \"test\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) + ), + //this is just to log details of each request stats + JtlWriter("jtls") + ).Run(); + Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); + } +} +``` + +> **Java 8+ is required** for test plan execution. + +More examples can be found in [tests](Abstracta.JmeterDsl.Tests) + +[Here](https://github.com/abstracta/jmeter-dotnet-dsl-sample) is a sample project for reference or for starting new projects from scratch. + +> **Tip 1:** When working with multiple samplers in a test plan, specify their names to easily check their respective statistics. + +> **Tip 2:** Since JMeter uses [log4j2](https://logging.apache.org/log4j/2.x/), if you want to control the logging level or output, you can use something similar to the tests included [log4j2.xml](Abstracta.JmeterDsl.Tests/log4j2.xml), using "CopyToOutputDirectory" in the project item so the file is available in dotnet build output directory as well (check [Abstracta.JmeterDsl.Test/Abstracta.JmeterDsl.Tests.csproj]). + + +**Check [here](https://abstracta.github.io/jmeter-dotnet-dsl/) for details on some interesting use cases**, like running tests at scale in [Azure Load Testing](https://azure.microsoft.com/en-us/products/load-testing/), and general usage guides. + +## Why? + +Check more about the motivation and analysis of alternatives [here](https://abstracta.github.io/jmeter-java-dsl/motivation/) + +## Support + +Join our [Discord server](https://discord.gg/WNSn5hqmSd) to engage with fellow JMeter DSL enthusiasts, ask questions, and share experiences. Visit [GitHub Issues](https://github.com/abstracta/jmeter-dotnet-dsl/issues) or [GitHub Discussions](https://github.com/abstracta/jmeter-dotnet-dsl/discussions) for bug reports, feature requests and share ideas. + +[Abstracta](https://abstracta.us), the main supporter for JMeter DSL development, offers enterprise-level support. Get faster response times, personalized customizations and consulting. + +For detailed support information, visit our [Support](https://abstracta.github.io/jmeter-dotnet-dsl/support) page. + +## Articles & Talks + +Check articles and talks mentioning the Java version [here](https://github.com/abstracta/jmeter-java-dsl#articles--talks). + +## Ecosystem + +* [Jmeter Java DSL](https://abstracta.github.io/jmeter-java-dsl): Java API which is the base of the .Net API. +* [pymeter](https://github.com/eldaduzman/pymeter): Python API based on JMeter Java DSL that allows Python devs to create and run JMeter test plans. + +## Contributing & Requesting features + +Currently, the project covers some of the most used features of JMeter and JMeter Java DSL test, but not everything, as we keep improving it to cover more use cases. + +We invest in the development of DSL according to the community's (your) interest, which we evaluate by reviewing GitHub stars' evolution, feature requests, and contributions. + +To keep improving the DSL we need you to **please create an issue for any particular feature or need that you have**. + +We also really appreciate pull requests. Check the [CONTRIBUTING](CONTRIBUTING.md) guide for an explanation of the main library components and how you can extend the library. diff --git a/StyleCop.ruleset b/StyleCop.ruleset new file mode 100644 index 0000000..e3da3b7 --- /dev/null +++ b/StyleCop.ruleset @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/.vuepress/client.ts b/docs/.vuepress/client.ts new file mode 100644 index 0000000..45cd832 --- /dev/null +++ b/docs/.vuepress/client.ts @@ -0,0 +1,12 @@ +import { defineClientConfig } from '@vuepress/client' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' + +library.add(faDiscord, faGithub) + +export default defineClientConfig({ + enhance({ app }) { + app.component('font-awesome-icon', FontAwesomeIcon) + }, +}) \ No newline at end of file diff --git a/docs/.vuepress/components/AutoLink.vue b/docs/.vuepress/components/AutoLink.vue new file mode 100644 index 0000000..67c37ad --- /dev/null +++ b/docs/.vuepress/components/AutoLink.vue @@ -0,0 +1,123 @@ + + + + + \ No newline at end of file diff --git a/docs/.vuepress/components/HomeFeatures.vue b/docs/.vuepress/components/HomeFeatures.vue new file mode 100644 index 0000000..3a44c02 --- /dev/null +++ b/docs/.vuepress/components/HomeFeatures.vue @@ -0,0 +1,24 @@ + + + diff --git a/docs/.vuepress/components/HomeHero.vue b/docs/.vuepress/components/HomeHero.vue new file mode 100644 index 0000000..91e788d --- /dev/null +++ b/docs/.vuepress/components/HomeHero.vue @@ -0,0 +1,95 @@ + + + diff --git a/docs/.vuepress/components/NavbarBrand.vue b/docs/.vuepress/components/NavbarBrand.vue new file mode 100644 index 0000000..82132a6 --- /dev/null +++ b/docs/.vuepress/components/NavbarBrand.vue @@ -0,0 +1,57 @@ + + + diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts new file mode 100644 index 0000000..3427243 --- /dev/null +++ b/docs/.vuepress/config.ts @@ -0,0 +1,105 @@ +import { defineUserConfig } from '@vuepress/cli' +import { defaultTheme } from '@vuepress/theme-default' +import { getDirname, path, fs } from '@vuepress/utils' +import { searchPlugin } from '@vuepress/plugin-search' +import { mediumZoomPlugin } from '@vuepress/plugin-medium-zoom' +import { containerPlugin } from '@vuepress/plugin-container' +import { mdEnhancePlugin } from "vuepress-plugin-md-enhance" +import { repoLinkSolverPlugin } from "./plugins/repoLinkSolverPlugin" +import { includedRelativeLinkSolverPlugin } from "./plugins/includedRelativeLinkSolverPlugin" +import { copyCodePlugin } from "vuepress-plugin-copy-code2" + +const __dirname = getDirname(import.meta.url) + +const REPO_LINK = "https://github.com/abstracta/jmeter-dotnet-dsl" + + +export default defineUserConfig({ + lang: 'en-US', + title: 'jmeter-dotnet-dsl', + description: 'Simple JMeter performance tests API', + base: '/jmeter-dotnet-dsl/', + head: [ + ['link', { rel: 'shortcut icon', href: '/jmeter-dotnet-dsl/favicon.ico'}], + // when changing this remember also changing components/NavbarBrand.vue + ['script', {}, `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': + new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], + j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= + 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); + })(window,document,'script','dataLayer','GTM-PHSGKLD'); + `] + ], + // restrict pattern to avoid going into included pages + pagePatterns: ["*.md", "*/index.md", "!.vuepress", "!node_modules"], + markdown: { + headers: { + level: [2, 3, 4] + } + }, + theme: defaultTheme({ + logo: '/logo.svg', + editLink: false, + lastUpdated: false, + contributors: false, + navbar: [ + { + text: 'Guide', + link: '/guide/', + }, + { + text: 'Support', + link: '/support/', + }, + { + text: 'Motivation', + link: 'https://abstracta.github.io/jmeter-java-dsl/motivation/', + }, + { + link: "https://discord.gg/WNSn5hqmSd", + icon: ['fab', 'discord'] + }, + { + link: REPO_LINK, + icon: ['fab', 'github'] + } + ], + sidebarDepth: 3 + }), + alias: { + '@theme/NavbarBrand.vue': path.resolve(__dirname, './components/NavbarBrand.vue'), + '@theme/AutoLink.vue': path.resolve(__dirname, './components/AutoLink.vue'), + '@theme/HomeHero.vue': path.resolve(__dirname, './components/HomeHero.vue'), + '@theme/HomeFeatures.vue': path.resolve(__dirname, './components/HomeFeatures.vue'), + }, + plugins: [ + searchPlugin({ maxSuggestions: 10 }), + mdEnhancePlugin({ + include: { + deep: true, + resolveImagePath: true, + resolveLinkPath: true, + resolvePath: (filePath: string, cwd: string | null) => { + let ret = path.join(cwd, filePath) + if (!fs.existsSync(ret)) { + throw new Error(`File ${ret} not found.`) + } + return ret; + } + } + }), + repoLinkSolverPlugin({ repoUrl: REPO_LINK }), + includedRelativeLinkSolverPlugin({}), + copyCodePlugin({ pure: true }), + containerPlugin({ + type: 'grid', + before: (info: string): string => `
\n`, + after: (): string => '
\n' + }), + containerPlugin({ + type: 'grid-logo', + before: (info: string): string => `\n' + }), + mediumZoomPlugin({selector: "*:is(img):not(.card img):not(a img)"}), + ], +}) \ No newline at end of file diff --git a/docs/.vuepress/plugins/includedRelativeLinkSolverPlugin.ts b/docs/.vuepress/plugins/includedRelativeLinkSolverPlugin.ts new file mode 100644 index 0000000..ed1012a --- /dev/null +++ b/docs/.vuepress/plugins/includedRelativeLinkSolverPlugin.ts @@ -0,0 +1,16 @@ +import mditPlugin from 'markdown-it-replace-link' + +export const includedRelativeLinkSolverPlugin = ({}) => ({ + name: 'includedRelativeLinkSolverPlugin', + extendsMarkdown: (md) => { + md.use(mditPlugin, { + replaceLink: (link, env) => { + if (link.startsWith('.')) { + let hashPos = link.indexOf('#') + return hashPos > 0 ? link.substring(hashPos) : link + } + return link + } + }) + } +}); diff --git a/docs/.vuepress/plugins/repoLinkSolverPlugin.ts b/docs/.vuepress/plugins/repoLinkSolverPlugin.ts new file mode 100644 index 0000000..034331a --- /dev/null +++ b/docs/.vuepress/plugins/repoLinkSolverPlugin.ts @@ -0,0 +1,10 @@ +import mditPlugin from 'markdown-it-replace-link' + +export const repoLinkSolverPlugin = ({repoUrl} : {repoUrl: String}) => ({ + name: 'repoLinkSolverPlugin', + extendsMarkdown: (md) => { + md.use(mditPlugin, { + replaceLink: (link, env) => link.startsWith('/') ? repoUrl + '/tree/master' + link : link + }) + } +}); diff --git a/docs/.vuepress/public/favicon.ico b/docs/.vuepress/public/favicon.ico new file mode 100644 index 0000000..75de640 Binary files /dev/null and b/docs/.vuepress/public/favicon.ico differ diff --git a/docs/.vuepress/public/logo.svg b/docs/.vuepress/public/logo.svg new file mode 100644 index 0000000..744869d --- /dev/null +++ b/docs/.vuepress/public/logo.svg @@ -0,0 +1,22 @@ + + + + + + diff --git a/docs/.vuepress/styles/index.scss b/docs/.vuepress/styles/index.scss new file mode 100644 index 0000000..87e99f1 --- /dev/null +++ b/docs/.vuepress/styles/index.scss @@ -0,0 +1,77 @@ +:root { + --c-brand: #a502ce; + --c-brand-light: #b55ecb; + + --c-shadow: rgba(0,0,0,0.2); +} + +html.dark { + // brand colors + --c-brand: #b55ecb; + --c-brand-light: #a502ce; + + --c-shadow: rgba(255,255,255,0.2); +} + +/* This is a fix to avoid, when building (does not happen with dev server), +showing two external link icons for external links containing icons.*/ +a.external-link>svg~span:nth-child(2) { + display: none; +} + +.vertical-divider { + display: inline-flex; + width:0; + border: solid; + border-width: 0 thin 0 0; + border-color: var(--c-border); + min-height: 80%; + vertical-align: text-bottom; + margin: 0 0.5rem; +} + +.hero-logo { + margin: 3rem auto 1.5rem; + display: flex; + align-items: center; + justify-content: center; +} + +.hero-logo span { + font-weight:500; + font-size: 3rem; + color: var(--c-text-accent); + margin: 0 0 0 0.7rem; +} + +.grid { + display: flex; +} + +.grid-logo { + display: flex; + align-items: center; + width: 25%; + height: 100px; + margin-right: 10px; + padding: 10px; + border: 1px solid var(--c-border); + border-radius: 5px; + box-shadow: 0 4px 8px 0 var(--c-shadow); +} + +.grid-logo a { + width: 100%; +} + +.grid-logo img { + width: 100%; + -webkit-filter: drop-shadow(1px 0 0 white) + drop-shadow(0 1px 0 white) + drop-shadow(-1px 0 0 white) + drop-shadow(0 -1px 0 white); + -filter: drop-shadow(1px 0 0 white) + drop-shadow(0 1px 0 white) + drop-shadow(-1px 0 0 white) + drop-shadow(0 -1px 0 white); +} diff --git a/docs/guide/debugging/debug-jmeter.md b/docs/guide/debugging/debug-jmeter.md new file mode 100644 index 0000000..b57ecc9 --- /dev/null +++ b/docs/guide/debugging/debug-jmeter.md @@ -0,0 +1,31 @@ +### Debug JMeter code + +You can even add breakpoints to JMeter or JMeter Java DSL code in your IDE and debug the code line by line providing the greatest possible detail. + +Here is an example screenshot debugging HTTP Sampler: + +![JMeter HTTP Sampler debugging in IDE](./images/jmeter-http-sampler-debugging.png) + +For that, you need to: + +* have a Java IDE with JMeter or JMeter Java DSL code open. +* set proper breakpoints in the code you are interested in debugging. +* can configure Remote JVM Debug like in the following screenshot: + ![IntelliJ Remote JVM Debug](./images/intellij-remote-jvm-debug.png) +* set required JVM arguments in the JMeter .Net DSL test using `EmbeddedJmeterEngine` like in the following example: + ```cs + TestPlan( + ThreadGroup(threads: 1, iterations: 1, + HttpSampler("http://my.service") + ) + ).RunIn(new EmbeddedJmeterEngine() + .JvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")); + ``` + > Note that we changed the `suspend` flag to `y` to block test execution until Remote JVM Debug is run in IDE. +* run the JMeter .Net DSL test. The test should get stuck until you start Remote JVM Debug in the Java IDE. +* start the Remote JVM Devug in the Java IDE. +* wait for a breakpoint to activate and debug as usual 🙂. + +::: tip +JMeter class in charge of executing threads logic is `org.apache.jmeter.threads.JMeterThread`. You can check the classes used by each DSL-provided test element by checking the Java DSL code. +::: diff --git a/docs/guide/debugging/dummy-sampler.md b/docs/guide/debugging/dummy-sampler.md new file mode 100644 index 0000000..3f55acc --- /dev/null +++ b/docs/guide/debugging/dummy-sampler.md @@ -0,0 +1,29 @@ +### Dummy sampler + +In many cases, you want to be able to test part of the test plan but without directly interacting with the service under test, avoiding any potential traffic to the servers, testing some border cases which might be difficult to reproduce with the actual server, and avoid actual server interactions variability and potential unpredictability. In such scenarios, you might replace actual samplers with `DummySampler` (which uses [Dummy Sampler plugin](https://jmeter-plugins.org/wiki/DummySampler/)) to be able to test extractors, assertions, controllers conditions, and other parts of the test plan under certain conditions/results generated by the samplers. + +Here is an example: + +```cs +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + TestPlan( + ThreadGroup(2, 10, + // HttpSampler("http://my.service") + DummySampler("{\"status\" : \"OK\"}") + ) + ).Run(); + } +} +``` + +::: tip +The DSL configures dummy samplers by default, in contrast to what JMeter does, with response time simulation disabled. This allows to speed up the debugging process, not having to wait for proper response time simulation (sleeps/waits). If you want a more accurate emulation, you might turn it on through the `ResponseTimeSimulation()` method. +::: + +Check [DslDummySampler](/Abstracta.JmeterDsl/Core/Samplers/DslDummySampler.cs) for more information o additional configuration and options. diff --git a/docs/guide/debugging/images/intellij-remote-jvm-debug.png b/docs/guide/debugging/images/intellij-remote-jvm-debug.png new file mode 100644 index 0000000..e4bfedf Binary files /dev/null and b/docs/guide/debugging/images/intellij-remote-jvm-debug.png differ diff --git a/docs/guide/debugging/images/jmeter-http-sampler-debugging.png b/docs/guide/debugging/images/jmeter-http-sampler-debugging.png new file mode 100644 index 0000000..388fbd8 Binary files /dev/null and b/docs/guide/debugging/images/jmeter-http-sampler-debugging.png differ diff --git a/docs/guide/debugging/images/test-plan-gui.png b/docs/guide/debugging/images/test-plan-gui.png new file mode 100644 index 0000000..5de4198 Binary files /dev/null and b/docs/guide/debugging/images/test-plan-gui.png differ diff --git a/docs/guide/debugging/images/view-results-tree.png b/docs/guide/debugging/images/view-results-tree.png new file mode 100644 index 0000000..77bcf22 Binary files /dev/null and b/docs/guide/debugging/images/view-results-tree.png differ diff --git a/docs/guide/debugging/index.md b/docs/guide/debugging/index.md new file mode 100644 index 0000000..8dd15d9 --- /dev/null +++ b/docs/guide/debugging/index.md @@ -0,0 +1,8 @@ +## Test plan debugging + +A usual requirement while building a test plan is to be able to review requests and responses and debug the test plan for potential issues in the configuration or behavior of the service under test. With JMeter DSL you have several options for this purpose. + + + + + diff --git a/docs/guide/debugging/show-in-gui.md b/docs/guide/debugging/show-in-gui.md new file mode 100644 index 0000000..303bc25 --- /dev/null +++ b/docs/guide/debugging/show-in-gui.md @@ -0,0 +1,28 @@ +### Test plan review un JMeter GUI + +A usual requirement for new DSL users that are used to Jmeter GUI, is to be able to review Jmeter DSL generated test plan in the familiar JMeter GUI. For this, you can use the `ShowInGui()` method in a test plan to open JMeter GUI with the preloaded test plan. + +This can be also used to debug the test plan, by adding elements (like view results tree, dummy samplers, etc.) in the GUI and running the test plan. + +Here is a simple example using the method: + +```cs +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + var stats = TestPlan( + ThreadGroup(2, 10, + HttpSampler("http://my.service") + ) + ).ShowInGui(); + } +} +``` + +Which ends up opening a window like this one: + +![Test plan in JMeter GUI](./images/test-plan-gui.png) \ No newline at end of file diff --git a/docs/guide/debugging/view-results-tree.md b/docs/guide/debugging/view-results-tree.md new file mode 100644 index 0000000..b5085be --- /dev/null +++ b/docs/guide/debugging/view-results-tree.md @@ -0,0 +1,41 @@ +### View results tree + +One option is using provided `ResultsTreeVisualizer()` like in the following example: + +```cs +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + var stats = TestPlan( + ThreadGroup(2, 10, + HttpSampler("http://my.service") + ), + ResultsTreeVisualizer() + ).Run(); + } +} +``` + +This will display the JMeter built-in View Results Tree element, which allows you to review request and response contents in addition to collected metrics (spent time, sent & received bytes, etc.) for each request sent to the server, in a window like this one: + +![View Results Tree GUI](./images/view-results-tree.png) + +::: tip +To debug test plans use a few iterations and threads to reduce the execution time and ease tracing by having less information to analyze. +::: + +::: tip +When adding `ResultsTreeVisualizer()` as a child of a thread group, it will only display sample results of that thread group. When added as a child of a sampler, it will only show sample results for that sampler. You can use this to only review certain sample results in your test plan. +::: + +::: tip +**Remove `ResultsTreeVisualizer()` from test plans when are no longer needed** (when debugging is finished). Leaving them might interfere with unattended test plan execution (eg: in CI) due to test plan execution not finishing until all visualizers windows are closed. +::: + +::: warning +By default, View Results Tree only displays the last 500 sample results. If you need to display more elements, use provided `ResultsLimit(int)` method which allows changing this value. Take into consideration that the more results are shown, the more memory that will require. So use this setting with care. +::: \ No newline at end of file diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 0000000..75749ec --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,22 @@ +# User guide + +Here we share some tips and examples on how to use the DSL to tackle common use cases. + +Provided examples use [Nunit](https://nunit.org/), but you can use other test libraries. + +Explore the DSL in your preferred IDE to discover all available features, and consider reviewing [existing tests](/Abstracta.JmeterDsl.Tests) for additional examples. + +The .Net DSL currently does not support all use cases supported by the [Java Dsl](https://abstracta.github.io/jmeter-java-dsl/), and currently only focuses on a limited set of features that cover the most commonly used cases. If you identify any particular scenario (or JMeter feature) that you need and is not currently supported, or easy to use, **please let us know by [creating an issue](https://github.com/abstracta/jmeter-dotnet-dsl/issues)** and we will try to implement it as soon as possible. Usually porting JMeter features is quite fast, and porting existing Java DSL features is even faster. + +::: tip +If you like this project, **please give it a star ⭐ in [GitHub](https://github.com/abstracta/jmeter-dotnet-dsl)!**. This helps the project be more visible, gain relevance and encourages us to invest more effort in new features. +::: + +For an intro to JMeter concepts and components, you can check [JMeter official documentation](http://jmeter.apache.org/usermanual/get-started.html). + + + + + + + diff --git a/docs/guide/protocols/http/cookies-and-cache.md b/docs/guide/protocols/http/cookies-and-cache.md new file mode 100644 index 0000000..f11468e --- /dev/null +++ b/docs/guide/protocols/http/cookies-and-cache.md @@ -0,0 +1,13 @@ +#### Cookies & caching + +JMeter DSL automatically adds a cookie manager and cache manager for automatic HTTP cookie and caching handling, emulating a browser behavior. If you need to disable them you can use something like this: + +```cs +TestPlan( + HttpCookies().Disable(), + HttpCache().Disable(), + ThreadGroup(2, 10, + HttpSampler("http://my.service") + ) +) +``` diff --git a/docs/guide/protocols/http/headers.md b/docs/guide/protocols/http/headers.md new file mode 100644 index 0000000..8bac223 --- /dev/null +++ b/docs/guide/protocols/http/headers.md @@ -0,0 +1,31 @@ +#### Headers + +You might have already noticed in some of the examples that we have shown, some ways to set some headers. For instance, in the following snippet, `Content-Type` header is being set in two different ways: + +```cs +HttpSampler("http://my.service") + .Post("{\"field\":\"val\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) +HttpSampler("http://my.service") + .ContentType(new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) +``` + +These are handy methods to specify the `Content-Type` header, but you can also set any header on a particular request using provided `Header` method, like this: + +```cs +HttpSampler("http://my.service") + .Header("X-First-Header", "val1") + .Header("X-Second-Header", "val2") +``` + +Additionally, you can specify headers to be used by all samplers in a test plan, thread group, transaction controllers, etc. For this, you can use `HttpHeaders` like this: + +```cs +TestPlan( + ThreadGroup(2, 10, + HttpHeaders() + .Header("X-Header", "val1"), + HttpSampler("http://my.service"), + HttpSampler("http://my.service/users") + ) +).Run(); +``` diff --git a/docs/guide/protocols/http/index.md b/docs/guide/protocols/http/index.md new file mode 100644 index 0000000..4eca1d7 --- /dev/null +++ b/docs/guide/protocols/http/index.md @@ -0,0 +1,9 @@ +### HTTP + +Throughout this guide, several examples have been shown for simple cases of HTTP requests (mainly how to do gets and posts), but the DSL provides additional features that you might need to be aware of. + +Here we show some of them, but check [JmeterDsl](/Abstracta.JmeterDsl/JmeterDsl.cs) and [DslHttpSampler](/Abstracta.JmeterDsl/Http/DslHttpSampler.cs) to explore all available features. + + + + diff --git a/docs/guide/protocols/http/methods-and-body.md b/docs/guide/protocols/http/methods-and-body.md new file mode 100644 index 0000000..1583966 --- /dev/null +++ b/docs/guide/protocols/http/methods-and-body.md @@ -0,0 +1,18 @@ +#### Methods & body + +As previously seen, you can do simple gets and posts like in the following snippet: + +```cs +HttpSampler("http://my.service") // A simple get +HttpSampler("http://my.service") + .Post("{\"field\":\"val\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) // simple post +``` + +But you can also use additional methods to specify any HTTP method and body: + +```cs +HttpSampler("http://my.service") + .Method(HttpMethod.Put.Method) + .ContentType(new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) + .Body("{\"field\":\"val\"}") +``` diff --git a/docs/guide/protocols/index.md b/docs/guide/protocols/index.md new file mode 100644 index 0000000..1580d4d --- /dev/null +++ b/docs/guide/protocols/index.md @@ -0,0 +1,3 @@ +## Protocols + + diff --git a/docs/guide/reporting/index.md b/docs/guide/reporting/index.md new file mode 100644 index 0000000..69fe01e --- /dev/null +++ b/docs/guide/reporting/index.md @@ -0,0 +1,5 @@ +## Reporting + +Once you have a test plan you would usually want to be able to analyze the collected information. This section contains a few ways to achieve this, but in the future, we plan to support more ([as Java DSL does](https://abstracta.github.io/jmeter-java-dsl/guide/#reporting)). If you are interested in some not yet covered feature, please ask for it by creating an [issue in the repository](https://github.com/abstracta/jmeter-dotnet-dsl/issues). + + diff --git a/docs/guide/reporting/logging.md b/docs/guide/reporting/logging.md new file mode 100644 index 0000000..4a0e551 --- /dev/null +++ b/docs/guide/reporting/logging.md @@ -0,0 +1,57 @@ +### Log requests and responses + +The main mechanism provided by JMeter (and `Abstracta.JmeterDsl`) to get information about generated requests, responses, and associated metrics is through the generation of JTL files. + +This can be easily achieved by using provided `JtlWriter` like in this example: + +```cs +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + var stats = TestPlan( + ThreadGroup(2, 10, + HttpSampler("http://my.service") + ), + JtlWriter("jtls") + ).Run(); + } +} +``` + +::: tip +By default, `JtlWriter` will write the most used information to evaluate the performance of the tested service. If you want to trace all the information of each request you may use `JtlWriter` with the `WithAllFields()` option. Doing this will provide all the information at the cost of additional computation and resource usage (fewer resources for actual load testing). You can tune which fields to include or not with `JtlWriter` and only log what you need, check [JtlWriter](/Abstracta.JmeterDsl/Core/Listeners/JtlWriter.cs) for more details. +::: + +::: tip +`JtlWriter` will automatically generate `.jtl` files applying this format: ` .jtl`. + +If you need a specific file name, for example for later postprocessing logic (eg: using CI build ID), you can specify it by using `JtlWriter(directory, fileName)`. + +When specifying the file name, make sure to use unique names, otherwise, the JTL contents may be appended to previous existing jtl files. +::: + +An additional option, specially targeted towards logging sample responses, is `ResponseFileSaver` which automatically generates a file for each received response. Here is an example: + +```cs +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + TestPlan( + ThreadGroup(2, 10, + HttpSampler("http://my.service") + ), + ResponseFileSaver(DateTime.Now.ToString("dd-MM-yyyy HH:mm:ss").Replace(":", "-") + "-response") + ).Run(); + } +} +``` + +Check [ResponseFileSaver](/Abstracta.JmeterDsl/Core/Listeners/ResponseFileSaver.cs) for more details. diff --git a/docs/guide/scale/azure.md b/docs/guide/scale/azure.md new file mode 100644 index 0000000..f232bc1 --- /dev/null +++ b/docs/guide/scale/azure.md @@ -0,0 +1,63 @@ +### Azure Load Testing + +To use [Azure Load Testing](https://azure.microsoft.com/en-us/products/load-testing/) to execute your test plans at scale is as easy as including the following package to your project: + +```powershell +dotnet add package Abstracta.JmeterDsl.Azure --version 0.1 +``` + +And using the provided engine like this: + +```cs +using Abstracta.JmeterDsl.Azure; +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + var stats = TestPlan( + ThreadGroup(2, 10, + HttpSampler("http://my.service") + ) + ).RunIn(new AzureEngine(System.getenv("AZURE_CREDS")) // AZURE_CREDS=tenantId:clientId:secretId + .TestName("dsl-test") + /* + This specifies the number of engine instances used to execute the test plan. + In this case, means that it will run 2(threads in thread group)x2(engines)=4 concurrent users/threads in total. + Each engine executes the test plan independently. + */ + .Engines(2) + .TestTimeout(Duration.ofMinutes(20))); + Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); + } +} +``` +> This test is using `AZURE_CREDS`, a custom environment variable containing `tenantId:clientId:clientSecret` with proper values for each. Check at [Azure Portal tenant properties](https://portal.azure.com/#view/Microsoft_AAD_IAM/TenantPropertiesBlade) the proper tenant ID for your subscription, and follow [this guide](https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal) to register an application with proper permissions and secrets generation for tests execution. + +With Azure, you can not only run the test at scale but also get additional features like nice real-time reporting, historic data tracking, etc. Here is an example of how a test looks like in Azure Load Testing: + +![Azure Load Testing Example Execution Dashboard](./azure.png) + +Check [AzureEngine](/Abstracta.JmeterDsl.Azure/AzureEngine.cs) for details on usage and available settings when running tests in Azure Load Testing. + +::: warning +By default, the engine is configured to time out if test execution takes more than 1 hour. +This timeout exists to avoid any potential problem with Azure Load Testing execution not detected by the +client, and avoid keeping the test indefinitely running until is interrupted by a user, +which may incur unnecessary expenses in Azure and is especially annoying when running tests +in an automated fashion, for example in CI/CD. +It is strongly advised to **set this timeout properly in each run**, according to the expected test +execution time plus some additional margin (to consider for additional delays in Azure Load Testing +test setup and teardown) to avoid unexpected test plan execution failure (due to timeout) or +unnecessary waits when there is some unexpected issue with Azure Load Testing execution. +::: + +::: tip +If you want to get debug logs for HTTP calls to Azure API, you can include the following setting to an existing `log4j2.xml` configuration file: +```xml + + +``` +::: diff --git a/docs/guide/scale/azure.png b/docs/guide/scale/azure.png new file mode 100644 index 0000000..74a685a Binary files /dev/null and b/docs/guide/scale/azure.png differ diff --git a/docs/guide/scale/blazemeter.md b/docs/guide/scale/blazemeter.md new file mode 100644 index 0000000..3382b7b --- /dev/null +++ b/docs/guide/scale/blazemeter.md @@ -0,0 +1,69 @@ +### BlazeMeter + +By including the following package: + +```powershell +dotnet add package Abstracta.JmeterDsl.BlazeMeter --version 0.1 +``` + +You can easily run a JMeter test plan at scale in [BlazeMeter](https://www.blazemeter.com/) like this: + +```cs +using Abstracta.JmeterDsl.BlazeMeter; +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + var stats = TestPlan( + // number of threads and iterations are in the end overwritten by BlazeMeter engine settings + ThreadGroup(2, 10, + HttpSampler("http://my.service") + ) + ).RunIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN")) + .TestName("DSL test") + .TotalUsers(500) + .HoldFor(TimeSpan.FromMinutes(10)) + .ThreadsPerEngine(100) + .TestTimeout(TimeSpan.FromMinutes(20)) + .TestName("dsl-test")); + Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); + } +} +``` + +> This test is using `BZ_TOKEN`, a custom environment variable with `:` format, to get the BlazeMeter API authentication credentials. + +Note that is as simple as [generating a BlazeMeter authentication token](https://guide.blazemeter.com/hc/en-us/articles/115002213289-BlazeMeter-API-keys-) and adding `.RunIn(new BlazeMeterEngine(...))` to any existing JMeter DSL test to get it running at scale in BlazeMeter. + +BlazeMeter will not only allow you to run the test at scale but also provides additional features like nice real-time reporting, historic data tracking, etc. Here is an example of how a test would look in BlazeMeter: + +![BlazeMeter Example Execution Dashboard](./blazemeter.png) + +Check [BlazeMeterEngine](/Abstracta.JmeterDsl.BlazeMeter/BlazeMeterEngine.cs) for details on usage and available settings when running tests in BlazeMeter. + +::: warning +By default the engine is configured to timeout if test execution takes more than 1 hour. +This timeout exists to avoid any potential problem with BlazeMeter execution not detected by the +client, and avoid keeping the test indefinitely running until is interrupted by a user, +which may incur in unnecessary expenses in BlazeMeter and is specially annoying when running tests +in automated fashion, for example in CI/CD. +It is strongly advised to **set this timeout properly in each run**, according to the expected test +execution time plus some additional margin (to consider for additional delays in BlazeMeter +test setup and teardown) to avoid unexpected test plan execution failure (due to timeout) or +unnecessary waits when there is some unexpected issue with BlazeMeter execution. +::: + +::: warning +`BlazeMeterEngine` always returns 0 as `sentBytes` statistics since there is no efficient way to get it from BlazMeter. +::: + +::: tip +In case you want to get debug logs for HTTP calls to BlazeMeter API, you can include the following setting to an existing `log4j2.xml` configuration file: +```xml + + +``` +::: diff --git a/docs/guide/scale/blazemeter.png b/docs/guide/scale/blazemeter.png new file mode 100644 index 0000000..dd86cec Binary files /dev/null and b/docs/guide/scale/blazemeter.png differ diff --git a/docs/guide/scale/index.md b/docs/guide/scale/index.md new file mode 100644 index 0000000..aaf70f7 --- /dev/null +++ b/docs/guide/scale/index.md @@ -0,0 +1,6 @@ +## Run test at scale + +Running a load test from one machine is not always enough, since you are limited to the machine's hardware capabilities. Sometimes, is necessary to run the test using a cluster of machines to be able to generate enough load for the system under test. Currently, the .Net DSL only provides two ways to run tests at scale, but in the future, we plan to support more ([as Java DSL does](https://abstracta.github.io/jmeter-java-dsl/guide/#run-test-at-scale)). If you are interested in some not yet covered feature, please ask for it by creating an [issue in the repository](https://github.com/abstracta/jmeter-dotnet-dsl/issues). + + + diff --git a/docs/guide/setup.md b/docs/guide/setup.md new file mode 100644 index 0000000..b00e550 --- /dev/null +++ b/docs/guide/setup.md @@ -0,0 +1,15 @@ +## Setup + +To use the DSL just include it in your project: + +```powershell +dotnet add package Abstracta.JmeterDsl --version 0.1 +``` + +::: tip +[Here](https://github.com/abstracta/jmeter-dotnet-dsl-sample) is a sample project in case you want to start one from scratch. +::: + +::: warning +JMeter .Net DSL uses existing JMeter Java DSL which in turn uses JMeter. JMeter Java DSL and JMeter are Java based tools. So, **Java 8+ is required** for the proper execution of DSL test plans. One option is downloading a JVM from [Adoptium](https://adoptium.net/) if you don't have one already. +::: \ No newline at end of file diff --git a/docs/guide/simple-test-plan.md b/docs/guide/simple-test-plan.md new file mode 100644 index 0000000..505dd81 --- /dev/null +++ b/docs/guide/simple-test-plan.md @@ -0,0 +1,83 @@ +## Simple HTTP test plan + +To generate HTTP requests just use provided `HttpSampler`. + +The following example uses 2 threads (concurrent users) that send 10 HTTP GET requests each to `http://my.service`. + +Additionally, it logs collected statistics (response times, status codes, etc.) to a file (for later analysis if needed) and checks that the response time 99 percentile is less than 5 seconds. + +```cs +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + var stats = TestPlan( + ThreadGroup(2, 10, + HttpSampler("http://my.service") + ), + //this is just to log details of each request stats + JtlWriter("jtls") + ).Run(); + Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); + } +} +``` + +::: tip +When working with multiple samplers in a test plan, specify their names (eg: `HttpSampler("home", "http://my.service")`) to easily check their respective statistics. +::: + +::: tip +JMeter .Net DSL uses Java for executing JMeter test plans. If you need to tune JVM parameters, for example for specifying maximum heap memory size, you can use `EmbeddedJMeterEngine` and the `JvmArgs` method like in the following example: + +```cs +using Abstracta.JmeterDsl.Core.Engines; +... +var stats = TestPlan( + ThreadGroup(2, 10, + HttpSampler("http://my.service") + ) +).RunIn(new EmbeddedJmeterEngine() + .JvmArgs("-Xmx4g") +); +``` +::: + +::: tip +Since JMeter uses [log4j2](https://logging.apache.org/log4j/2.x/), if you want to control the logging level or output, you can use something similar to this [log4j2.xml](Abstracta.JmeterDsl.Tests/log4j2.xml), using "CopyToOutputDirectory" in the project item, so the file is available in dotnet build output directory as well (check [Abstracta.JmeterDsl.Test/Abstracta.JmeterDsl.Tests.csproj]). +::: + +::: tip +Depending on the test framework you use, and the way you run your tests, you might be able to see JMeter logs and output in real-time, at the end of the test, or not see them at all. This is not something we can directly control in JMeter DSL, and heavily depends on the dotnet environment and testing framework implementation. + +When using Nunit, to get real-time console output from JMeter you might want to run your tests with something like `dotnet test -v n` and add the following code to your tests: + +```cs +private TextWriter? originalConsoleOut; + +// Redirecting output to progress to get live stdout with nunit. +// https://github.com/nunit/nunit3-vs-adapter/issues/343 +// https://github.com/nunit/nunit/issues/1139 +[SetUp] +public void SetUp() +{ + originalConsoleOut = Console.Out; + Console.SetOut(TestContext.Progress); +} + +[TearDown] +public void TearDown() +{ + Console.SetOut(originalConsoleOut!); +} +``` +::: + +::: tip +Keep in mind that you can use .Net programming to modularize and create abstractions which allow you to build complex test plans that are still easy to read, use and maintain. [Here is an example](https://github.com/abstracta/jmeter-java-dsl/issues/26#issuecomment-953783407) of some complex abstraction built using Java features (you can easily extrapolate to .Net) and the DSL. +::: + +Check [HTTP performance testing](./protocols/http/index#http) for additional details while testing HTTP services. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..a48a76b --- /dev/null +++ b/docs/index.md @@ -0,0 +1,50 @@ +--- +home: true +heroHeight: 68 +heroImage: /logo.svg +actions: + - text: User Guide → + link: /guide/ +features: +- title: 💙 Git, IDE & Programmers Friendly + details: Simple way of defining performance tests that takes advantage of IDEs autocompletion and inline documentation. +- title: 💪 JMeter ecosystem & community + details: Use the most popular performance tool and take advantage of the wide support of protocols and tools. +- title: 😎 Built-in features & extensibility + details: Built-in additional features which ease usage and using it in CI/CD pipelines. +footer: Made by Abstracta with ❤️ | Apache 2.0 Licensed | Powered by Vuepress +footerHtml: true +--- + +## Example + +Add the package to your project: + +```powershell +dotnet add package Abstracta.JmeterDsl --version 0.1 +``` + +Create performance test: + +```cs +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + var stats = TestPlan( + ThreadGroup(2, 10, + HttpSampler("http://my.service") + ) + ).Run(); + Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); + } +} +``` + +> **Java 8+ is required** for test plan execution. + +[Here](https://github.com/abstracta/jmeter-dotnet-dsl-sample) is a sample project in case you want to start one from scratch. + diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..702ee36 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,31 @@ +{ + "name": "jmeter-dotnet-dsl", + "version": "1.0.0", + "description": "Simple JMeter performance tests API", + "main": "index.js", + "repository": "https://github.com/abstracta/jmeter-dotnet-dsl", + "license": "Apache-2.0", + "devDependencies": { + "@fortawesome/fontawesome-svg-core": "^6.4.0", + "@fortawesome/free-brands-svg-icons": "^6.4.0", + "@fortawesome/vue-fontawesome": "^3.0.3", + "@vuepress/cli": "2.0.0-beta.63", + "@vuepress/client": "2.0.0-beta.63", + "@vuepress/plugin-container": "2.0.0-beta.63", + "@vuepress/plugin-medium-zoom": "2.0.0-beta.63", + "@vuepress/plugin-search": "2.0.0-beta.63", + "@vuepress/shared": "2.0.0-beta.63", + "@vuepress/theme-default": "2.0.0-beta.63", + "@vuepress/utils": "2.0.0-beta.63", + "markdown-it-replace-link": "^1.2.0", + "vue": "^3.3.4", + "vue-router": "^4.2.3", + "vuepress": "2.0.0-beta.63", + "vuepress-plugin-copy-code2": "2.0.0-beta.228", + "vuepress-plugin-md-enhance": "2.0.0-beta.228" + }, + "scripts": { + "dev": "vuepress dev . --clean-cache --clean-temp", + "build": "vuepress build . --clean-cache --clean-temp" + } +} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 0000000..429da00 --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,3374 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + '@fortawesome/fontawesome-svg-core': + specifier: ^6.4.0 + version: 6.4.0 + '@fortawesome/free-brands-svg-icons': + specifier: ^6.4.0 + version: 6.4.0 + '@fortawesome/vue-fontawesome': + specifier: ^3.0.3 + version: 3.0.3(@fortawesome/fontawesome-svg-core@6.4.0)(vue@3.3.4) + '@vuepress/cli': + specifier: 2.0.0-beta.63 + version: 2.0.0-beta.63 + '@vuepress/client': + specifier: 2.0.0-beta.63 + version: 2.0.0-beta.63 + '@vuepress/plugin-container': + specifier: 2.0.0-beta.63 + version: 2.0.0-beta.63 + '@vuepress/plugin-medium-zoom': + specifier: 2.0.0-beta.63 + version: 2.0.0-beta.63 + '@vuepress/plugin-search': + specifier: 2.0.0-beta.63 + version: 2.0.0-beta.63 + '@vuepress/shared': + specifier: 2.0.0-beta.63 + version: 2.0.0-beta.63 + '@vuepress/theme-default': + specifier: 2.0.0-beta.63 + version: 2.0.0-beta.63 + '@vuepress/utils': + specifier: 2.0.0-beta.63 + version: 2.0.0-beta.63 + markdown-it-replace-link: + specifier: ^1.2.0 + version: 1.2.0 + vue: + specifier: ^3.3.4 + version: 3.3.4 + vue-router: + specifier: ^4.2.3 + version: 4.2.3(vue@3.3.4) + vuepress: + specifier: 2.0.0-beta.63 + version: 2.0.0-beta.63(@vuepress/client@2.0.0-beta.63)(vue@3.3.4) + vuepress-plugin-copy-code2: + specifier: 2.0.0-beta.228 + version: 2.0.0-beta.228(vuepress@2.0.0-beta.63) + vuepress-plugin-md-enhance: + specifier: 2.0.0-beta.228 + version: 2.0.0-beta.228(vuepress@2.0.0-beta.63) + +packages: + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 + dev: true + + /@babel/code-frame@7.22.5: + resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.22.5 + dev: true + + /@babel/compat-data@7.22.5: + resolution: {integrity: sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.22.5: + resolution: {integrity: sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.22.5 + '@babel/generator': 7.22.5 + '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) + '@babel/helper-module-transforms': 7.22.5 + '@babel/helpers': 7.22.5 + '@babel/parser': 7.22.5 + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.5 + '@babel/types': 7.22.5 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.22.5: + resolution: {integrity: sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.5 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 + jsesc: 2.5.2 + dev: true + + /@babel/helper-compilation-targets@7.22.5(@babel/core@7.22.5): + resolution: {integrity: sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.22.5 + '@babel/core': 7.22.5 + '@babel/helper-validator-option': 7.22.5 + browserslist: 4.21.9 + lru-cache: 5.1.1 + semver: 6.3.0 + dev: true + + /@babel/helper-environment-visitor@7.22.5: + resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.22.5: + resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.5 + '@babel/types': 7.22.5 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.5 + dev: true + + /@babel/helper-module-imports@7.22.5: + resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.5 + dev: true + + /@babel/helper-module-transforms@7.22.5: + resolution: {integrity: sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-module-imports': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.5 + '@babel/helper-validator-identifier': 7.22.5 + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.5 + '@babel/types': 7.22.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.5 + dev: true + + /@babel/helper-split-export-declaration@7.22.5: + resolution: {integrity: sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.5 + dev: true + + /@babel/helper-string-parser@7.22.5: + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.5: + resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.22.5: + resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers@7.22.5: + resolution: {integrity: sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.5 + '@babel/types': 7.22.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight@7.22.5: + resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.5 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.22.5: + resolution: {integrity: sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.22.5 + dev: true + + /@babel/template@7.22.5: + resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.5 + '@babel/parser': 7.22.5 + '@babel/types': 7.22.5 + dev: true + + /@babel/traverse@7.22.5: + resolution: {integrity: sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.5 + '@babel/generator': 7.22.5 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.5 + '@babel/parser': 7.22.5 + '@babel/types': 7.22.5 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.22.5: + resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.5 + to-fast-properties: 2.0.0 + dev: true + + /@braintree/sanitize-url@6.0.2: + resolution: {integrity: sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==} + dev: true + + /@esbuild/android-arm64@0.17.19: + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.17.19: + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.17.19: + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.17.19: + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.17.19: + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.17.19: + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.17.19: + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.17.19: + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.17.19: + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.17.19: + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.17.19: + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.17.19: + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.17.19: + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.17.19: + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.17.19: + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.17.19: + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.17.19: + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.17.19: + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.17.19: + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.17.19: + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.17.19: + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.17.19: + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@fortawesome/fontawesome-common-types@6.4.0: + resolution: {integrity: sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==} + engines: {node: '>=6'} + requiresBuild: true + dev: true + + /@fortawesome/fontawesome-svg-core@6.4.0: + resolution: {integrity: sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + '@fortawesome/fontawesome-common-types': 6.4.0 + dev: true + + /@fortawesome/free-brands-svg-icons@6.4.0: + resolution: {integrity: sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + '@fortawesome/fontawesome-common-types': 6.4.0 + dev: true + + /@fortawesome/vue-fontawesome@3.0.3(@fortawesome/fontawesome-svg-core@6.4.0)(vue@3.3.4): + resolution: {integrity: sha512-KCPHi9QemVXGMrfuwf3nNnNo129resAIQWut9QTAMXmXqL2ErABC6ohd2yY5Ipq0CLWNbKHk8TMdTXL/Zf3ZhA==} + peerDependencies: + '@fortawesome/fontawesome-svg-core': ~1 || ~6 + vue: '>= 3.0.0 < 4' + dependencies: + '@fortawesome/fontawesome-svg-core': 6.4.0 + vue: 3.3.4 + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.18 + dev: true + + /@jridgewell/resolve-uri@3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.18: + resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /@kurkle/color@0.3.2: + resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} + dev: true + + /@mdit-vue/plugin-component@0.12.0: + resolution: {integrity: sha512-LrwV3f0Y6H7b7m/w1Y3bkGuR3HOiBK4QiHHW3HuRMza6MZodDQbj8Baik5/V5GiSg1/ltijS1CymVcycd1EfTw==} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit-vue/plugin-frontmatter@0.12.0: + resolution: {integrity: sha512-26Y3JktjGgNoCVH7NLqi5RcdAauAqxepTt2qXueRcRHtGpiRQV2/M1FveIhCOTCtHSuG5bBOHUxGaV6vRK3Vbw==} + dependencies: + '@mdit-vue/types': 0.12.0 + '@types/markdown-it': 12.2.3 + gray-matter: 4.0.3 + markdown-it: 13.0.1 + dev: true + + /@mdit-vue/plugin-headers@0.12.0: + resolution: {integrity: sha512-7qR63J2uc/rXbjHT77WoYBm9imwzx1tVESmRK+Uth6kqFvSWAXAFPcm4PBatGEE8TgzhklPs5BTcQtQhmmsyaw==} + dependencies: + '@mdit-vue/shared': 0.12.0 + '@mdit-vue/types': 0.12.0 + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit-vue/plugin-sfc@0.12.0: + resolution: {integrity: sha512-mH+rHsERzDxGucAQJILspRiD723AIWMmtMhp7lDKdkCIbIhYfupFv/CkSeX+LAx5UY5greWvUTPGYVKn4gw/5Q==} + dependencies: + '@mdit-vue/types': 0.12.0 + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit-vue/plugin-title@0.12.0: + resolution: {integrity: sha512-XrQcior1EmPgsDG88KsoF4LUSQw/RS1Nyfn5xNWGiurO70a2hml4kCe0XzT4sLKUAPG0HNbIY6b92ezNezqWTg==} + dependencies: + '@mdit-vue/shared': 0.12.0 + '@mdit-vue/types': 0.12.0 + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit-vue/plugin-toc@0.12.0: + resolution: {integrity: sha512-tT985CqvLp17DFWHrSvmmJbh7qcy0Rl0dBbYN//Fn952a04dbr1mb2LqW0B1oStSAQj2q24HpK4ZPgYOt7Z1Jg==} + dependencies: + '@mdit-vue/shared': 0.12.0 + '@mdit-vue/types': 0.12.0 + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit-vue/shared@0.12.0: + resolution: {integrity: sha512-E+sGSubhvnp+Gmb2hJXFDxdLwwQD1H52EVbA4yrxxI5q/cwtnPIN2eJU3zlZB9KcvzXYDFFwt/x2mfhK8RZKBg==} + dependencies: + '@mdit-vue/types': 0.12.0 + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit-vue/types@0.12.0: + resolution: {integrity: sha512-mrC4y8n88BYvgcgzq9bvTlDgFyi2zuvzmPilRvRc3Uz1iIvq8mDhxJ0rHKFUNzPEScpDvJdIujqiDrulMqiudA==} + dev: true + + /@mdit/plugin-align@0.4.8: + resolution: {integrity: sha512-n6dNMqXb2wZmQ2dod8fq18ehEq+KtMNFoDpC6H3oCaAv/kXT7fYSry0fqrFBP5I3l8yevrgAwo+zZC+c3cyZig==} + engines: {node: '>= 14'} + dependencies: + '@mdit/plugin-container': 0.4.8 + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-attrs@0.4.8: + resolution: {integrity: sha512-SB2yTHRNG8j5shh1TtJAPuPFWaMeQp6P/9ieLVPFdXLU6RPobEwf1GAX39YDaIKaWXEmkEJJdKFClOKmyWd9BQ==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-container@0.4.8: + resolution: {integrity: sha512-ruiP9XrJ6Uaru/9ZO7iBGm96Fiqr/4Ecn6zHER3/GzWpRJ9oPjrDBWoQ9eFrmINoq1C89puZG0lmAJJ9KCTeAw==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-figure@0.4.8: + resolution: {integrity: sha512-fzFwKlE34pnenqAshqHtCrgv5Ro9QE0Cjd0BR/wxkFCy4ZyyVHZUNA007HOz/j9t5ryVimdZQPcqfcQEcBk8sA==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-footnote@0.4.8: + resolution: {integrity: sha512-D2OOOoiMEdgI4p5NAtAK8wjOK3th4qIB6ZkOZ38USN+nzTwNy51Prq/elKiqhEd95q0BtWobrPsrY7qO1BW7kA==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-img-lazyload@0.4.8: + resolution: {integrity: sha512-GGppqJQhl5pZ2CftLxstxMVSZQCdOiJB/1aKEMjpi+EehYV1MlKPzaQp+XTyVDJAkv/k6pe+91ZnsSZgHnIUcA==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-img-mark@0.4.8: + resolution: {integrity: sha512-00zkJ3cIW1R5O+lk/WHuhOrHFdO17TVVxfBN8mhzH6S17W+2KqBMcBv5fpxi7g3R95rZ1fAZ6T1I5lg069RBkA==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-img-size@0.4.8: + resolution: {integrity: sha512-+fkNRrhkwZgIRJi6ucginEzy95pmhekOer23gBbOOezZev9D4XpA1tFhLAu1srvUVAKh+JmRXiVJUT71Xw9LTg==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-include@0.4.8: + resolution: {integrity: sha512-Hd+ZjisjjUS6ZRtjXUkfbYx3HpGKAY4XVpzmvhinK4+EPqiW4SrQor4G03ckpYu2fFjBF6u6+NbMtkHD8dcMZQ==} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + upath: 2.0.1 + dev: true + + /@mdit/plugin-katex@0.4.8: + resolution: {integrity: sha512-IQUfqpRp+/0gq0VDUOLI0xVvAaiHQv91f6PFBuRG2mvxSsJBECCWZTiJpCgriL7XHSVeSI8zHEYsha9UR674nw==} + engines: {node: '>= 14'} + dependencies: + '@mdit/plugin-tex': 0.4.8 + '@types/katex': 0.16.0 + '@types/markdown-it': 12.2.3 + katex: 0.16.8 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-mark@0.4.8: + resolution: {integrity: sha512-51sV7MsPPoW+oa47mwUoD44a3N6XcnYBCOixuDtPzpmKH7ueUJ/ULOGJoBsbveo/ZqTCivJ+3cwoTujaGua8mQ==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-mathjax@0.4.8: + resolution: {integrity: sha512-eFFYR6Qo9eZnS+3vUVIHd1lLasx6Upybu3tvdNJ119CUkVd3edtvDqI286RJuApfyDM0uAzkqEgmSKCr4pT8NA==} + engines: {node: '>= 14'} + dependencies: + '@mdit/plugin-tex': 0.4.8 + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + mathjax-full: 3.2.2 + upath: 2.0.1 + dev: true + + /@mdit/plugin-stylize@0.4.8: + resolution: {integrity: sha512-Wjo3hEHGybu+2ubLaUY52g5SCk6ThFwHYQAYScB7NX39lbr1xefVKs5RYeyH3xCRMdK3S5+b1mlklrdSARQ1fg==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-sub@0.4.8: + resolution: {integrity: sha512-U/6FtGgakdk/JhybHGHykBampF5YMZFkS1DB9uht/3uycWT4ejGefZ1XT9r59liQ3Bh/9CTy0niRNvMwdolPOA==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-sup@0.4.8: + resolution: {integrity: sha512-wv4n9PKoiXI2RFqUrqOSxcKl71mTNCzlNJNlb4WfF9OTIn1CXR298EeL6XnbgS6snLuraur15PgGqwWw6wP7AQ==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-tab@0.4.8: + resolution: {integrity: sha512-/YUI4KQAtHUE6AkJUfIEIKjnK8LEAkcBMe2z8SYmzeEs9U0vHvQNawUd6ANHOXrpeqyPrgQnhWqGkF4yMqfAjg==} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-tasklist@0.4.8: + resolution: {integrity: sha512-VAnCR4dnfqOpW1hPEAunJFVvV31eARnD23XPSK3JAQADUFtnileoR0OdXZATC4gTsuVnYh8V8d7rujjL1QvxQw==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-tex@0.4.8: + resolution: {integrity: sha512-HgWb8l0Can+NsxFfLu358Xwj1plxXHXf2YkjxM316pUeVZhNhjPjoqIpR46ebCwWbWW+GmwT0YdeUvQrDgM3ig==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@mdit/plugin-uml@0.4.8: + resolution: {integrity: sha512-X414T54zh0i+n5MbPL0kzGwRzcCU0hlpe4wp74cr44RWrsvJ8+78ioOx7WJOM8rgGHRWIoEEp6BjB1WfI734Iw==} + engines: {node: '>= 14'} + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@types/debug@4.1.8: + resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} + dependencies: + '@types/ms': 0.7.31 + dev: true + + /@types/fs-extra@11.0.1: + resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==} + dependencies: + '@types/jsonfile': 6.1.1 + '@types/node': 20.3.2 + dev: true + + /@types/hash-sum@1.0.0: + resolution: {integrity: sha512-FdLBT93h3kcZ586Aee66HPCVJ6qvxVjBlDWNmxSGSbCZe9hTsjRKdSsl4y1T+3zfujxo9auykQMnFsfyHWD7wg==} + dev: true + + /@types/js-yaml@4.0.5: + resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} + dev: true + + /@types/jsonfile@6.1.1: + resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==} + dependencies: + '@types/node': 20.3.2 + dev: true + + /@types/katex@0.16.0: + resolution: {integrity: sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw==} + dev: true + + /@types/linkify-it@3.0.2: + resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==} + dev: true + + /@types/markdown-it-emoji@2.0.2: + resolution: {integrity: sha512-2ln8Wjbcj/0oRi/6VnuMeWEHHuK8uapFttvcLmDIe1GKCsFBLOLBX+D+xhDa9oWOQV0IpvxwrSfKKssAqqroog==} + dependencies: + '@types/markdown-it': 12.2.3 + dev: true + + /@types/markdown-it@12.2.3: + resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} + dependencies: + '@types/linkify-it': 3.0.2 + '@types/mdurl': 1.0.2 + dev: true + + /@types/mdast@3.0.11: + resolution: {integrity: sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==} + dependencies: + '@types/unist': 2.0.6 + dev: true + + /@types/mdurl@1.0.2: + resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} + dev: true + + /@types/ms@0.7.31: + resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} + dev: true + + /@types/node@20.3.2: + resolution: {integrity: sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==} + dev: true + + /@types/raphael@2.3.3: + resolution: {integrity: sha512-Rhvq0q6wzyvipejki/9w87/pgapyE+s3gO66tdl1oD3qDrow+ek+4vVYAbRkeL58HCCK9EOZKwyjqYJ/TFkmtQ==} + dev: true + + /@types/unist@2.0.6: + resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} + dev: true + + /@types/web-bluetooth@0.0.17: + resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==} + dev: true + + /@vitejs/plugin-vue@4.2.3(vite@4.3.9)(vue@3.3.4): + resolution: {integrity: sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.0.0 + vue: ^3.2.25 + dependencies: + vite: 4.3.9 + vue: 3.3.4 + dev: true + + /@vue/compiler-core@3.3.4: + resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} + dependencies: + '@babel/parser': 7.22.5 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + source-map-js: 1.0.2 + dev: true + + /@vue/compiler-dom@3.3.4: + resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} + dependencies: + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 + dev: true + + /@vue/compiler-sfc@3.3.4: + resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} + dependencies: + '@babel/parser': 7.22.5 + '@vue/compiler-core': 3.3.4 + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-ssr': 3.3.4 + '@vue/reactivity-transform': 3.3.4 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + magic-string: 0.30.0 + postcss: 8.4.24 + source-map-js: 1.0.2 + dev: true + + /@vue/compiler-ssr@3.3.4: + resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==} + dependencies: + '@vue/compiler-dom': 3.3.4 + '@vue/shared': 3.3.4 + dev: true + + /@vue/devtools-api@6.5.0: + resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==} + dev: true + + /@vue/reactivity-transform@3.3.4: + resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==} + dependencies: + '@babel/parser': 7.22.5 + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + magic-string: 0.30.0 + dev: true + + /@vue/reactivity@3.3.4: + resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} + dependencies: + '@vue/shared': 3.3.4 + dev: true + + /@vue/repl@1.5.0(vue@3.3.4): + resolution: {integrity: sha512-qFqKtvA2FM9viYXzbWrpGrL8mDGswsqDsEjfaibr/YOqeza7i49VmO0AKPrOdQDOS2qmq9uV+G6OPX1rGhUSIQ==} + peerDependencies: + vue: ^3.2.13 + dependencies: + vue: 3.3.4 + dev: true + + /@vue/runtime-core@3.3.4: + resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==} + dependencies: + '@vue/reactivity': 3.3.4 + '@vue/shared': 3.3.4 + dev: true + + /@vue/runtime-dom@3.3.4: + resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==} + dependencies: + '@vue/runtime-core': 3.3.4 + '@vue/shared': 3.3.4 + csstype: 3.1.2 + dev: true + + /@vue/server-renderer@3.3.4(vue@3.3.4): + resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==} + peerDependencies: + vue: 3.3.4 + dependencies: + '@vue/compiler-ssr': 3.3.4 + '@vue/shared': 3.3.4 + vue: 3.3.4 + dev: true + + /@vue/shared@3.3.4: + resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} + dev: true + + /@vuepress/bundler-vite@2.0.0-beta.63: + resolution: {integrity: sha512-g02mgffks+kxcnmKLyBSa8WvtKa+La1y0xQ3bLgvHb54CGHUALdxiBh83sPh5zFvnWoGgUGU9enc6MCB68dFzA==} + dependencies: + '@vitejs/plugin-vue': 4.2.3(vite@4.3.9)(vue@3.3.4) + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + autoprefixer: 10.4.14(postcss@8.4.24) + connect-history-api-fallback: 2.0.0 + postcss: 8.4.24 + postcss-load-config: 4.0.1(postcss@8.4.24) + rollup: 3.25.3 + vite: 4.3.9 + vue: 3.3.4 + vue-router: 4.2.3(vue@3.3.4) + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + - ts-node + dev: true + + /@vuepress/cli@2.0.0-beta.63: + resolution: {integrity: sha512-F4hoRvNkf/nJsNEW5OmoJAMiUQZ6bx5A/yCN8be2tT5bTZQwfW4K9CDQXMJEoDBecepsyx3PjPqC5vQx2aR8Xw==} + hasBin: true + dependencies: + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + cac: 6.7.14 + chokidar: 3.5.3 + envinfo: 7.10.0 + esbuild: 0.17.19 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/client@2.0.0-beta.63: + resolution: {integrity: sha512-L/EQt3ZBKtEB6HUUifPSn/rAQM1R2ndKOqGYHzdG90WrUbzpM+ROm+3BxpVinvUuKkRgnXyf8qgaETlvUNw90Q==} + dependencies: + '@vue/devtools-api': 6.5.0 + '@vuepress/shared': 2.0.0-beta.63 + vue: 3.3.4 + vue-router: 4.2.3(vue@3.3.4) + dev: true + + /@vuepress/core@2.0.0-beta.63: + resolution: {integrity: sha512-9n8/ke7qSJ1M+eeJGIcgPBtSr9aPUO+WJMg1Zg82UfazI7EcshRFHO/CsGGUw1RPm6VtKM5E9iUgl3YJpM9ZvA==} + dependencies: + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/markdown': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + vue: 3.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/markdown@2.0.0-beta.63: + resolution: {integrity: sha512-XLR/bNgfKmmz3YL57vG0Du8C6KQVJuFPIKQSwI6caWsl02ddbdt8IAO7PhczH6dfuEX3TFdQkx70sEgM6pdhKA==} + dependencies: + '@mdit-vue/plugin-component': 0.12.0 + '@mdit-vue/plugin-frontmatter': 0.12.0 + '@mdit-vue/plugin-headers': 0.12.0 + '@mdit-vue/plugin-sfc': 0.12.0 + '@mdit-vue/plugin-title': 0.12.0 + '@mdit-vue/plugin-toc': 0.12.0 + '@mdit-vue/shared': 0.12.0 + '@mdit-vue/types': 0.12.0 + '@types/markdown-it': 12.2.3 + '@types/markdown-it-emoji': 2.0.2 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + markdown-it: 13.0.1 + markdown-it-anchor: 8.6.7(@types/markdown-it@12.2.3)(markdown-it@13.0.1) + markdown-it-emoji: 2.0.2 + mdurl: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-active-header-links@2.0.0-beta.63: + resolution: {integrity: sha512-xTV+8bhflukXoN3tJWJlL7EvBfTH0dP0mmIszlo5OELDxp2qiK/p+lZ9uCNv+zlymGzx1HfLatYRiOK10DwJTQ==} + dependencies: + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + ts-debounce: 4.0.0 + vue: 3.3.4 + vue-router: 4.2.3(vue@3.3.4) + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-back-to-top@2.0.0-beta.63: + resolution: {integrity: sha512-oT2xmTpnyuMMbyjefKJj3XcgMQe2LK31XsjjZiMZH1L4F2yx/mE0/x9pHkrioKyQE9J0TUlp6U91SfDjYiWQzA==} + dependencies: + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + ts-debounce: 4.0.0 + vue: 3.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-container@2.0.0-beta.63: + resolution: {integrity: sha512-zgQLfxCfp/EiXGBj29lT4fMQunKVuKDq33EDHvK83F8xGpvavqM/AWy+/mVxpin+KUyg45Oe8oNRG5ZTSEd0Xg==} + dependencies: + '@types/markdown-it': 12.2.3 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/markdown': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + markdown-it: 13.0.1 + markdown-it-container: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-external-link-icon@2.0.0-beta.63: + resolution: {integrity: sha512-KPclAh29fYdzsITlx5kSZGMSDdbRw2cQXMEKqiJfo7v1gIP7DJjeIxPsknarIFThmXv/SoKzli3RxxGQY6rcww==} + dependencies: + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/markdown': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + vue: 3.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-git@2.0.0-beta.63: + resolution: {integrity: sha512-VnhuRMzn/WwjS++Ci9E43pqZi4uj34baeRcNEz31gGI9MvtRYGSs5HXrsTydvwbyHhTQYF0vbcAsbIN2pp7DCw==} + dependencies: + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + execa: 7.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-medium-zoom@2.0.0-beta.63: + resolution: {integrity: sha512-n20+27k9i7IYbKVC4gDKD1gk7ZWAKTWiVpWHVWzDFLtpJOJALrIz3FEjdodIqsN3v83j3fzNIHWrunVkxQoc0w==} + dependencies: + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + medium-zoom: 1.0.8 + vue: 3.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-nprogress@2.0.0-beta.63: + resolution: {integrity: sha512-bts29KHmLeg3ao0IADnvdQaOV4IK+IRJek5lGtYiT/oq8r/9Mb0SeDut7UUsf0C/hZhKCDhpyTRYHLgEup5FVQ==} + dependencies: + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + vue: 3.3.4 + vue-router: 4.2.3(vue@3.3.4) + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-palette@2.0.0-beta.63: + resolution: {integrity: sha512-TOXZ23Q1ETFztmBtgDvB1Mvd16Hsw2pjO5d1fnO4YVnfJgW2fXsy7n6zXNYg6KgfTPM0dSlj2ngn+KbZa/w+mw==} + dependencies: + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + chokidar: 3.5.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-prismjs@2.0.0-beta.63: + resolution: {integrity: sha512-lGMwZf75ROgLaTS2V/ZfZc2RSBHxPZRTxFlv2lAk2m8eVaitwnIvLTZfr+rRukZDaifuqEdpsHgfxO5xTRp9Gg==} + dependencies: + '@vuepress/core': 2.0.0-beta.63 + prismjs: 1.29.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-search@2.0.0-beta.63: + resolution: {integrity: sha512-TgubdtTZ4VvZxhqq+kfv72y3CsprYocCAs3FtIOD/a4RgG/vH92xwHfEu/eL6iXvqMqrgf47PMI1tajuYwgBcA==} + dependencies: + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + chokidar: 3.5.3 + vue: 3.3.4 + vue-router: 4.2.3(vue@3.3.4) + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-theme-data@2.0.0-beta.63: + resolution: {integrity: sha512-JLNxa6YTa3WK45j/9sZQsBJGr7aASqQb0dt3CvIitUVHk1n0cpc14nUANbj+3VAQUx6C5qkYcpA9EdDNAUf14Q==} + dependencies: + '@vue/devtools-api': 6.5.0 + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + vue: 3.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/shared@2.0.0-beta.63: + resolution: {integrity: sha512-xJ90x2RAuYenM4Pn7gbhJ9lkmPBWscM0cff+ypEVI47oV0hKpaDpNLkteT4QjBoRIHyqlz5CFUP+2BMOk9zxCA==} + dependencies: + '@mdit-vue/types': 0.12.0 + '@vue/shared': 3.3.4 + dev: true + + /@vuepress/theme-default@2.0.0-beta.63: + resolution: {integrity: sha512-iry9+YGKC8tcZJ5pUCS/nwUdBlbC5YPkyarnu1bs1M2IvXv/a+PR3zxT/BMVdCtZlXk+CqZ1odsoSxXhFZEkxw==} + peerDependencies: + sass-loader: ^13.2.1 + peerDependenciesMeta: + sass-loader: + optional: true + dependencies: + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/plugin-active-header-links': 2.0.0-beta.63 + '@vuepress/plugin-back-to-top': 2.0.0-beta.63 + '@vuepress/plugin-container': 2.0.0-beta.63 + '@vuepress/plugin-external-link-icon': 2.0.0-beta.63 + '@vuepress/plugin-git': 2.0.0-beta.63 + '@vuepress/plugin-medium-zoom': 2.0.0-beta.63 + '@vuepress/plugin-nprogress': 2.0.0-beta.63 + '@vuepress/plugin-palette': 2.0.0-beta.63 + '@vuepress/plugin-prismjs': 2.0.0-beta.63 + '@vuepress/plugin-theme-data': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + '@vueuse/core': 10.2.1(vue@3.3.4) + sass: 1.63.6 + vue: 3.3.4 + vue-router: 4.2.3(vue@3.3.4) + transitivePeerDependencies: + - '@vue/composition-api' + - supports-color + dev: true + + /@vuepress/utils@2.0.0-beta.63: + resolution: {integrity: sha512-Ow3zmwiCrmpA/Ezs9PRIroa6HBSGjYGEo9kmC2Prfl4CkRTup/rhvVrrPpi3WkqT/B+fKgPQN8ihs5EYazxRtw==} + dependencies: + '@types/debug': 4.1.8 + '@types/fs-extra': 11.0.1 + '@types/hash-sum': 1.0.0 + '@vuepress/shared': 2.0.0-beta.63 + debug: 4.3.4 + fs-extra: 11.1.1 + globby: 13.2.0 + hash-sum: 2.0.0 + ora: 6.3.1 + picocolors: 1.0.0 + upath: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@vueuse/core@10.2.1(vue@3.3.4): + resolution: {integrity: sha512-c441bfMbkAwTNwVRHQ0zdYZNETK//P84rC01aP2Uy/aRFCiie9NE/k9KdIXbno0eDYP5NPUuWv0aA/I4Unr/7w==} + dependencies: + '@types/web-bluetooth': 0.0.17 + '@vueuse/metadata': 10.2.1 + '@vueuse/shared': 10.2.1(vue@3.3.4) + vue-demi: 0.14.5(vue@3.3.4) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + + /@vueuse/metadata@10.2.1: + resolution: {integrity: sha512-3Gt68mY/i6bQvFqx7cuGBzrCCQu17OBaGWS5JdwISpMsHnMKKjC2FeB5OAfMcCQ0oINfADP3i9A4PPRo0peHdQ==} + dev: true + + /@vueuse/shared@10.2.1(vue@3.3.4): + resolution: {integrity: sha512-QWHq2bSuGptkcxx4f4M/fBYC3Y8d3M2UYyLsyzoPgEoVzJURQ0oJeWXu79OiLlBb8gTKkqe4mO85T/sf39mmiw==} + dependencies: + vue-demi: 0.14.5(vue@3.3.4) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /autoprefixer@10.4.14(postcss@8.4.24): + resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.21.9 + caniuse-lite: 1.0.30001509 + fraction.js: 4.2.0 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: true + + /balloon-css@1.2.0: + resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /bl@5.1.0: + resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browserslist@4.21.9: + resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001509 + electron-to-chromium: 1.4.443 + node-releases: 2.0.12 + update-browserslist-db: 1.0.11(browserslist@4.21.9) + dev: true + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + + /caniuse-lite@1.0.30001509: + resolution: {integrity: sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA==} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@5.2.0: + resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: true + + /character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + dev: true + + /chart.js@4.3.0: + resolution: {integrity: sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==} + engines: {pnpm: '>=7'} + dependencies: + '@kurkle/color': 0.3.2 + dev: true + + /cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + dev: true + + /cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + restore-cursor: 4.0.0 + dev: true + + /cli-spinners@2.9.0: + resolution: {integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==} + engines: {node: '>=6'} + dev: true + + /clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: true + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: true + + /commander@9.2.0: + resolution: {integrity: sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==} + engines: {node: ^12.20.0 || >=14} + dev: true + + /connect-history-api-fallback@2.0.0: + resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} + engines: {node: '>=0.8'} + dev: true + + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: true + + /cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + dependencies: + layout-base: 1.0.2 + dev: true + + /cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + dependencies: + layout-base: 2.0.1 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: true + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + + /csstype@3.1.2: + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + dev: true + + /cytoscape-cose-bilkent@4.1.0(cytoscape@3.25.0): + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + dependencies: + cose-base: 1.0.3 + cytoscape: 3.25.0 + dev: true + + /cytoscape-fcose@2.2.0(cytoscape@3.25.0): + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + dependencies: + cose-base: 2.2.0 + cytoscape: 3.25.0 + dev: true + + /cytoscape@3.25.0: + resolution: {integrity: sha512-7MW3Iz57mCUo6JQCho6CmPBCbTlJr7LzyEtIkutG255HLVd4XuBg2I9BkTZLI/e4HoaOB/BiAzXuQybQ95+r9Q==} + engines: {node: '>=0.10'} + dependencies: + heap: 0.2.7 + lodash: 4.17.21 + dev: true + + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: true + + /d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + dev: true + + /d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: true + + /d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: true + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: true + + /d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: true + + /d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + dependencies: + delaunator: 5.0.0 + dev: true + + /d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + dev: true + + /d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + dev: true + + /d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + dev: true + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: true + + /d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + dependencies: + d3-dsv: 3.0.1 + dev: true + + /d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + dev: true + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: true + + /d3-geo@3.1.0: + resolution: {integrity: sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: true + + /d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + dev: true + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: true + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: true + + /d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + dev: true + + /d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + dev: true + + /d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + dev: true + + /d3-scale-chromatic@3.0.0: + resolution: {integrity: sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + dev: true + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: true + + /d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + dev: true + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: true + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: true + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: true + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: true + + /d3-transition@3.0.1(d3-selection@3.0.0): + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + dev: true + + /d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: true + + /d3@7.8.5: + resolution: {integrity: sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.0 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.0.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + dev: true + + /dagre-d3-es@7.0.10: + resolution: {integrity: sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==} + dependencies: + d3: 7.8.5 + lodash-es: 4.17.21 + dev: true + + /dayjs@1.11.8: + resolution: {integrity: sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==} + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + dependencies: + character-entities: 2.0.2 + dev: true + + /defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + dependencies: + clone: 1.0.4 + dev: true + + /delaunator@5.0.0: + resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==} + dependencies: + robust-predicates: 3.0.2 + dev: true + + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true + + /diff@5.1.0: + resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} + engines: {node: '>=0.3.1'} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /dompurify@3.0.3: + resolution: {integrity: sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ==} + dev: true + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true + + /echarts@5.4.2: + resolution: {integrity: sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==} + dependencies: + tslib: 2.3.0 + zrender: 5.4.3 + dev: true + + /electron-to-chromium@1.4.443: + resolution: {integrity: sha512-QG+DKVaD7OkcCJ/0x/IHdVEcwU7cak9Vr9dXCNp7G9ojBZQWtwtRV77CBOrU49jsKygedFcNc/IHUrGljKV2Gw==} + dev: true + + /elkjs@0.8.2: + resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} + dev: true + + /entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} + dev: true + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + + /envinfo@7.10.0: + resolution: {integrity: sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /esbuild@0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /eve-raphael@0.5.0: + resolution: {integrity: sha512-jrxnPsCGqng1UZuEp9DecX/AuSyAszATSjf4oEcRxvfxa1Oux4KkIPKBAAWWnpdwfARtr+Q0o9aPYWjsROD7ug==} + dev: true + + /execa@7.1.1: + resolution: {integrity: sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + dev: true + + /extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + dependencies: + is-extendable: 0.1.1 + dev: true + + /fast-glob@3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fflate@0.8.0: + resolution: {integrity: sha512-FAdS4qMuFjsJj6XHbBaZeXOgaypXp8iw/Tpyuq/w3XA41jjLHT8NPA+n7czH/DDhdncq0nAyDZmPeWXh2qmdIg==} + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /flowchart.ts@1.0.0: + resolution: {integrity: sha512-U8FN9kg/U1xPdQ5xW3e/hZBSX7y/07zGESCrJ2mjlT8CLuhzPXHXRJrJ+VyFW0DEJLdj4O7MvJImg3sXeRGt1A==} + dependencies: + '@types/raphael': 2.3.3 + raphael: 2.3.0 + tslib: 2.6.0 + dev: true + + /fraction.js@4.2.0: + resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + dev: true + + /fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globby@13.2.0: + resolution: {integrity: sha512-jWsQfayf13NvqKUIL3Ta+CIqMnvlaIDFveWE/dpOZ9+3AMEJozsxDvKA02zync9UuvOM8rOXzsD5GqKP4OnWPQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.2.12 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 4.0.0 + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /hash-sum@2.0.0: + resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + dev: true + + /heap@0.2.7: + resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} + dev: true + + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + requiresBuild: true + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: true + + /human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + dev: true + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /immutable@4.3.0: + resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==} + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /katex@0.16.8: + resolution: {integrity: sha512-ftuDnJbcbOckGY11OO+zg3OofESlbR5DRl2cmN8HeWeeFIV7wTXvAOx8kEjZjobhA+9wh2fbKeO6cdcA9Mnovg==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: true + + /khroma@2.0.0: + resolution: {integrity: sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==} + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: true + + /layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + dev: true + + /layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + dev: true + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /linkify-it@4.0.1: + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} + dependencies: + uc.micro: 1.0.6 + dev: true + + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /log-symbols@5.1.0: + resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} + engines: {node: '>=12'} + dependencies: + chalk: 5.2.0 + is-unicode-supported: 1.3.0 + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /magic-string@0.30.0: + resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /markdown-it-anchor@8.6.7(@types/markdown-it@12.2.3)(markdown-it@13.0.1): + resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} + peerDependencies: + '@types/markdown-it': '*' + markdown-it: '*' + dependencies: + '@types/markdown-it': 12.2.3 + markdown-it: 13.0.1 + dev: true + + /markdown-it-container@3.0.0: + resolution: {integrity: sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==} + dev: true + + /markdown-it-emoji@2.0.2: + resolution: {integrity: sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==} + dev: true + + /markdown-it-replace-link@1.2.0: + resolution: {integrity: sha512-tIsShJvQOYwTHjIPhE1SWo12HjOtpEqSQfVoApm3fuIkVcZrSE7zK3iW8kXcaJYasf8Ddf+VbH5fNXCzfejOrQ==} + optionalDependencies: + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + dev: true + + /markdown-it@13.0.1: + resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} + hasBin: true + dependencies: + argparse: 2.0.1 + entities: 3.0.1 + linkify-it: 4.0.1 + mdurl: 1.0.1 + uc.micro: 1.0.6 + dev: true + + /mathjax-full@3.2.2: + resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} + dependencies: + esm: 3.2.25 + mhchemparser: 4.2.1 + mj-context-menu: 0.6.1 + speech-rule-engine: 4.0.7 + dev: true + + /mdast-util-from-markdown@1.3.1: + resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + dependencies: + '@types/mdast': 3.0.11 + '@types/unist': 2.0.6 + decode-named-character-reference: 1.0.2 + mdast-util-to-string: 3.2.0 + micromark: 3.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-decode-string: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-stringify-position: 3.0.3 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + dev: true + + /mdast-util-to-string@3.2.0: + resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} + dependencies: + '@types/mdast': 3.0.11 + dev: true + + /mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + dev: true + + /medium-zoom@1.0.8: + resolution: {integrity: sha512-CjFVuFq/IfrdqesAXfg+hzlDKu6A2n80ZIq0Kl9kWjoHh9j1N9Uvk5X0/MmN0hOfm5F9YBswlClhcwnmtwz7gA==} + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /mermaid@10.2.3: + resolution: {integrity: sha512-cMVE5s9PlQvOwfORkyVpr5beMsLdInrycAosdr+tpZ0WFjG4RJ/bUHST7aTgHNJbujHkdBRAm+N50P3puQOfPw==} + dependencies: + '@braintree/sanitize-url': 6.0.2 + cytoscape: 3.25.0 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.25.0) + cytoscape-fcose: 2.2.0(cytoscape@3.25.0) + d3: 7.8.5 + dagre-d3-es: 7.0.10 + dayjs: 1.11.8 + dompurify: 3.0.3 + elkjs: 0.8.2 + khroma: 2.0.0 + lodash-es: 4.17.21 + mdast-util-from-markdown: 1.3.1 + non-layered-tidy-tree-layout: 2.0.2 + stylis: 4.3.0 + ts-dedent: 2.2.0 + uuid: 9.0.0 + web-worker: 1.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /mhchemparser@4.2.1: + resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} + dev: true + + /micromark-core-commonmark@1.1.0: + resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} + dependencies: + decode-named-character-reference: 1.0.2 + micromark-factory-destination: 1.1.0 + micromark-factory-label: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-factory-title: 1.1.0 + micromark-factory-whitespace: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-html-tag-name: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: true + + /micromark-factory-destination@1.1.0: + resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: true + + /micromark-factory-label@1.1.0: + resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: true + + /micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + dev: true + + /micromark-factory-title@1.1.0: + resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: true + + /micromark-factory-whitespace@1.1.0: + resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: true + + /micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: true + + /micromark-util-chunked@1.1.0: + resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} + dependencies: + micromark-util-symbol: 1.1.0 + dev: true + + /micromark-util-classify-character@1.1.0: + resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: true + + /micromark-util-combine-extensions@1.1.0: + resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-types: 1.1.0 + dev: true + + /micromark-util-decode-numeric-character-reference@1.1.0: + resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} + dependencies: + micromark-util-symbol: 1.1.0 + dev: true + + /micromark-util-decode-string@1.1.0: + resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 1.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-symbol: 1.1.0 + dev: true + + /micromark-util-encode@1.1.0: + resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} + dev: true + + /micromark-util-html-tag-name@1.2.0: + resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} + dev: true + + /micromark-util-normalize-identifier@1.1.0: + resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} + dependencies: + micromark-util-symbol: 1.1.0 + dev: true + + /micromark-util-resolve-all@1.1.0: + resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} + dependencies: + micromark-util-types: 1.1.0 + dev: true + + /micromark-util-sanitize-uri@1.2.0: + resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-encode: 1.1.0 + micromark-util-symbol: 1.1.0 + dev: true + + /micromark-util-subtokenize@1.1.0: + resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: true + + /micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + dev: true + + /micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + dev: true + + /micromark@3.2.0: + resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} + dependencies: + '@types/debug': 4.1.8 + debug: 4.3.4 + decode-named-character-reference: 1.0.2 + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-combine-extensions: 1.1.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-encode: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + + /mj-context-menu@0.6.1: + resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + dev: true + + /mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /node-releases@2.0.12: + resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} + dev: true + + /non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-path@5.1.0: + resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + + /ora@6.3.1: + resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + chalk: 5.2.0 + cli-cursor: 4.0.0 + cli-spinners: 2.9.0 + is-interactive: 2.0.0 + is-unicode-supported: 1.3.0 + log-symbols: 5.1.0 + stdin-discarder: 0.1.0 + strip-ansi: 7.1.0 + wcwidth: 1.0.1 + dev: true + + /parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + dev: true + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /postcss-load-config@4.0.1(postcss@8.4.24): + resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.24 + yaml: 2.3.1 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.24: + resolution: {integrity: sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /raphael@2.3.0: + resolution: {integrity: sha512-w2yIenZAQnp257XUWGni4bLMVxpUpcIl7qgxEgDIXtmSypYtlNxfXWpOBxs7LBTps5sDwhRnrToJrMUrivqNTQ==} + dependencies: + eve-raphael: 0.5.0 + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /reveal.js@4.5.0: + resolution: {integrity: sha512-Lx1hUWhJR7Y7ScQNyGt7TFzxeviDAswK2B0cn9RwbPZogTMRgS8+FTr+/12KNHOegjvWKH0H0EGwBARNDPTgWQ==} + engines: {node: '>=10.0.0'} + dev: true + + /robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + dev: true + + /rollup@3.25.3: + resolution: {integrity: sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + dev: true + + /sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /sass@1.63.6: + resolution: {integrity: sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.3.0 + source-map-js: 1.0.2 + dev: true + + /section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + dev: true + + /semver@6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + dev: true + + /semver@7.5.3: + resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /speech-rule-engine@4.0.7: + resolution: {integrity: sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==} + hasBin: true + dependencies: + commander: 9.2.0 + wicked-good-xpath: 1.3.0 + xmldom-sre: 0.1.31 + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /stdin-discarder@0.1.0: + resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + bl: 5.1.0 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + + /striptags@3.2.0: + resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} + dev: true + + /stylis@4.3.0: + resolution: {integrity: sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==} + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /ts-debounce@4.0.0: + resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==} + dev: true + + /ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + dev: true + + /tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + dev: true + + /tslib@2.6.0: + resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} + dev: true + + /uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + dev: true + + /unist-util-stringify-position@3.0.3: + resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} + dependencies: + '@types/unist': 2.0.6 + dev: true + + /universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + + /upath@2.0.1: + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} + dev: true + + /update-browserslist-db@1.0.11(browserslist@4.21.9): + resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.9 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /uuid@9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + dev: true + + /uvu@0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + dequal: 2.0.3 + diff: 5.1.0 + kleur: 4.1.5 + sade: 1.8.1 + dev: true + + /vite@4.3.9: + resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.17.19 + postcss: 8.4.24 + rollup: 3.25.3 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vue-demi@0.14.5(vue@3.3.4): + resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + vue: 3.3.4 + dev: true + + /vue-router@4.2.3(vue@3.3.4): + resolution: {integrity: sha512-ynQ/edCZNUC/9koONOSgxGJbEBXZ1nUA0lKI3xTiOd3Ywe4QRCf2q8pGCG1v5ovdzPggoq3M09FxNCZTM9pZfw==} + peerDependencies: + vue: ^3.2.0 + dependencies: + '@vue/devtools-api': 6.5.0 + vue: 3.3.4 + dev: true + + /vue@3.3.4: + resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} + dependencies: + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-sfc': 3.3.4 + '@vue/runtime-dom': 3.3.4 + '@vue/server-renderer': 3.3.4(vue@3.3.4) + '@vue/shared': 3.3.4 + dev: true + + /vuepress-plugin-copy-code2@2.0.0-beta.228(vuepress@2.0.0-beta.63): + resolution: {integrity: sha512-LeAPMw4TeUZk/hFjfeygdH9tHs6oL27+LNc2d1j72fma8es8tJICnmG1PAGOWEPsZZSRlBj5tF+O1M+PUeFMcw==} + engines: {node: '>=16.19.0', npm: '>=8', pnpm: '>=7'} + peerDependencies: + sass-loader: ^13.3.2 + vuepress: 2.0.0-beta.63 + vuepress-vite: 2.0.0-beta.63 + vuepress-webpack: 2.0.0-beta.63 + peerDependenciesMeta: + sass-loader: + optional: true + vuepress: + optional: true + vuepress-vite: + optional: true + vuepress-webpack: + optional: true + dependencies: + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + '@vueuse/core': 10.2.1(vue@3.3.4) + balloon-css: 1.2.0 + vue: 3.3.4 + vue-router: 4.2.3(vue@3.3.4) + vuepress: 2.0.0-beta.63(@vuepress/client@2.0.0-beta.63)(vue@3.3.4) + vuepress-plugin-sass-palette: 2.0.0-beta.228(vuepress@2.0.0-beta.63) + vuepress-shared: 2.0.0-beta.228(vuepress@2.0.0-beta.63) + transitivePeerDependencies: + - '@vue/composition-api' + - supports-color + dev: true + + /vuepress-plugin-md-enhance@2.0.0-beta.228(vuepress@2.0.0-beta.63): + resolution: {integrity: sha512-LOjuflbNEo86i4AcS6S2hY45KnZ2z9+4Ps5nNuTN+6VqDqF+Vs0t/a+qk9qNSgLsnVtG8ZVs0S7VnHFKyobn4A==} + engines: {node: '>=16.19.0', npm: '>=8', pnpm: '>=7'} + peerDependencies: + sass-loader: ^13.3.2 + vuepress: 2.0.0-beta.63 + vuepress-vite: 2.0.0-beta.63 + vuepress-webpack: 2.0.0-beta.63 + peerDependenciesMeta: + sass-loader: + optional: true + vuepress: + optional: true + vuepress-vite: + optional: true + vuepress-webpack: + optional: true + dependencies: + '@babel/core': 7.22.5 + '@mdit/plugin-align': 0.4.8 + '@mdit/plugin-attrs': 0.4.8 + '@mdit/plugin-container': 0.4.8 + '@mdit/plugin-figure': 0.4.8 + '@mdit/plugin-footnote': 0.4.8 + '@mdit/plugin-img-lazyload': 0.4.8 + '@mdit/plugin-img-mark': 0.4.8 + '@mdit/plugin-img-size': 0.4.8 + '@mdit/plugin-include': 0.4.8 + '@mdit/plugin-katex': 0.4.8 + '@mdit/plugin-mark': 0.4.8 + '@mdit/plugin-mathjax': 0.4.8 + '@mdit/plugin-stylize': 0.4.8 + '@mdit/plugin-sub': 0.4.8 + '@mdit/plugin-sup': 0.4.8 + '@mdit/plugin-tab': 0.4.8 + '@mdit/plugin-tasklist': 0.4.8 + '@mdit/plugin-tex': 0.4.8 + '@mdit/plugin-uml': 0.4.8 + '@types/js-yaml': 4.0.5 + '@types/markdown-it': 12.2.3 + '@vue/repl': 1.5.0(vue@3.3.4) + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + '@vueuse/core': 10.2.1(vue@3.3.4) + balloon-css: 1.2.0 + chart.js: 4.3.0 + echarts: 5.4.2 + flowchart.ts: 1.0.0 + js-yaml: 4.1.0 + katex: 0.16.8 + markdown-it: 13.0.1 + mermaid: 10.2.3 + reveal.js: 4.5.0 + vue: 3.3.4 + vue-router: 4.2.3(vue@3.3.4) + vuepress: 2.0.0-beta.63(@vuepress/client@2.0.0-beta.63)(vue@3.3.4) + vuepress-plugin-sass-palette: 2.0.0-beta.228(vuepress@2.0.0-beta.63) + vuepress-shared: 2.0.0-beta.228(vuepress@2.0.0-beta.63) + transitivePeerDependencies: + - '@vue/composition-api' + - supports-color + dev: true + + /vuepress-plugin-sass-palette@2.0.0-beta.228(vuepress@2.0.0-beta.63): + resolution: {integrity: sha512-MYhGOxWn/3JCnJpgJm7RfMDzpVuhjlOGILz3nHMEAoS8kYunqYmWxAz75ZlH6d5Xc+KPzwconviQ+3xuAx8nkg==} + engines: {node: '>=16.19.0', npm: '>=8', pnpm: '>=7'} + peerDependencies: + sass-loader: ^13.3.2 + vuepress: 2.0.0-beta.63 + vuepress-vite: 2.0.0-beta.63 + vuepress-webpack: 2.0.0-beta.63 + peerDependenciesMeta: + sass-loader: + optional: true + vuepress: + optional: true + vuepress-vite: + optional: true + vuepress-webpack: + optional: true + dependencies: + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + chokidar: 3.5.3 + sass: 1.63.6 + vuepress: 2.0.0-beta.63(@vuepress/client@2.0.0-beta.63)(vue@3.3.4) + vuepress-shared: 2.0.0-beta.228(vuepress@2.0.0-beta.63) + transitivePeerDependencies: + - '@vue/composition-api' + - supports-color + dev: true + + /vuepress-shared@2.0.0-beta.228(vuepress@2.0.0-beta.63): + resolution: {integrity: sha512-gwoAaGM6dzYJ004geaFwyfrjRlMg6iUrBCKZIs0ccZYpXLNZwFoxWyYpNjijbEky1B+E2ch6OnirWQ/aS7R7uQ==} + engines: {node: '>=16.19.0', npm: '>=8', pnpm: '>=7'} + peerDependencies: + vuepress: 2.0.0-beta.63 + vuepress-vite: 2.0.0-beta.63 + vuepress-webpack: 2.0.0-beta.63 + peerDependenciesMeta: + vuepress: + optional: true + vuepress-vite: + optional: true + vuepress-webpack: + optional: true + dependencies: + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/shared': 2.0.0-beta.63 + '@vuepress/utils': 2.0.0-beta.63 + '@vueuse/core': 10.2.1(vue@3.3.4) + cheerio: 1.0.0-rc.12 + dayjs: 1.11.8 + execa: 7.1.1 + fflate: 0.8.0 + gray-matter: 4.0.3 + semver: 7.5.3 + striptags: 3.2.0 + vue: 3.3.4 + vue-router: 4.2.3(vue@3.3.4) + vuepress: 2.0.0-beta.63(@vuepress/client@2.0.0-beta.63)(vue@3.3.4) + transitivePeerDependencies: + - '@vue/composition-api' + - supports-color + dev: true + + /vuepress-vite@2.0.0-beta.63(@vuepress/client@2.0.0-beta.63)(vue@3.3.4): + resolution: {integrity: sha512-TMPl/fQi6mqdHyz/NkZbkZHch4aFxmw+Um52vS09ryE4W4jDGK0CY+7nw8RuTkABN/02OzDoAqRaYfwZFXywFw==} + engines: {node: '>=16.19.0'} + hasBin: true + peerDependencies: + '@vuepress/client': 2.0.0-beta.63 + vue: ^3.3.4 + dependencies: + '@vuepress/bundler-vite': 2.0.0-beta.63 + '@vuepress/cli': 2.0.0-beta.63 + '@vuepress/client': 2.0.0-beta.63 + '@vuepress/core': 2.0.0-beta.63 + '@vuepress/theme-default': 2.0.0-beta.63 + vue: 3.3.4 + transitivePeerDependencies: + - '@types/node' + - '@vue/composition-api' + - less + - sass + - sass-loader + - stylus + - sugarss + - supports-color + - terser + - ts-node + dev: true + + /vuepress@2.0.0-beta.63(@vuepress/client@2.0.0-beta.63)(vue@3.3.4): + resolution: {integrity: sha512-P9AnMkrRHVl5kUlBcMbLMvkWoVFyxopQl9zLVWGnTRBXFqxY5IkfP+BvTVfLRXuW2itC11Q3TW6OnJtlY8V1sQ==} + engines: {node: '>=16.19.0'} + hasBin: true + dependencies: + vuepress-vite: 2.0.0-beta.63(@vuepress/client@2.0.0-beta.63)(vue@3.3.4) + transitivePeerDependencies: + - '@types/node' + - '@vue/composition-api' + - '@vuepress/client' + - less + - sass + - sass-loader + - stylus + - sugarss + - supports-color + - terser + - ts-node + - vue + dev: true + + /wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + dependencies: + defaults: 1.0.4 + dev: true + + /web-worker@1.2.0: + resolution: {integrity: sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==} + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wicked-good-xpath@1.3.0: + resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} + dev: true + + /xmldom-sre@0.1.31: + resolution: {integrity: sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==} + engines: {node: '>=0.1'} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yaml@2.3.1: + resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} + engines: {node: '>= 14'} + dev: true + + /zrender@5.4.3: + resolution: {integrity: sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==} + dependencies: + tslib: 2.3.0 + dev: true diff --git a/docs/support/abstracta-logo.png b/docs/support/abstracta-logo.png new file mode 100644 index 0000000..ea946ec Binary files /dev/null and b/docs/support/abstracta-logo.png differ diff --git a/docs/support/azure-logo.png b/docs/support/azure-logo.png new file mode 100644 index 0000000..915566d Binary files /dev/null and b/docs/support/azure-logo.png differ diff --git a/docs/support/blazemeter-logo.png b/docs/support/blazemeter-logo.png new file mode 100644 index 0000000..75cfe78 Binary files /dev/null and b/docs/support/blazemeter-logo.png differ diff --git a/docs/support/index.md b/docs/support/index.md new file mode 100644 index 0000000..bedb5cb --- /dev/null +++ b/docs/support/index.md @@ -0,0 +1,53 @@ +--- +sidebar: false +--- + +# Support + +## Community Support + +The JMeter DSL project has a vibrant and active community that provides extensive support, on a best effort basis, to its users. Community support is primarily offered through the following channels: + +* [Discord server]: Join our [Discord server] to engage with fellow JMeter DSL enthusiasts. It's a real-time platform where you can ask questions, share experiences, and participate in discussions. +* [GitHub Issues]: For bug reports, feature requests, or any specific problems you encounter while using JMeter DSL, [GitHub Issues] is the place to go. Create an issue, and the community will jump in to assist you, propose improvements, and collaborate on finding solutions. +* [GitHub Discussions]: If you have open-ended discussions, ideas, or suggestions related to JMeter DSL, head over to [GitHub Discussions]. It's an excellent platform for brainstorming, gathering feedback, and engaging in community-driven conversations. + +The community is actively involved in proposing new improvements, answering questions, assisting in design decisions, and submitting pull requests. Together, we strive to enhance the capabilities and usability of JMeter DSL. + +## Enterprise Support by Abstracta + +In addition to community support, [Abstracta](https://abstracta.us) offers enterprise-level support for JMeter DSL users. Abstracta is the main supporter of JMeter DSL development and provides specialized professional services to ensure the success of organizations using JMeter DSL. With Abstracta's enterprise support, you can accelerate your JMeter DSL implementation and have access to: + +* Dedicated support team : Get prompt answers and peace of mind from a dedicated support team with the expertise to help you resolve issues faster. +* Customizations: Receive tailored solutions to meet your specific requirements. +* Consulting services: Access a team of experts to fine-tune your JMeter DSL usage, speed up implementation, work on your performance testing strategy and overall testing processes. + +Abstracta is committed to helping organizations succeed with JMeter DSL by providing comprehensive support and specialized services tailored to your enterprise needs. + +To explore Abstracta's enterprise support options or discuss your specific needs, please [contact the Abstracta team](https://abstracta.us/contact-us). + +:::: grid +::: grid-logo https://abstracta.us +![Abstracta logo](./abstracta-logo.png) +::: +:::: + +## Industry Support + +JMeter DSL has received valuable support from industry-leading companies, contributing to the integration features and promoting the tool. We would like to acknowledge and express our gratitude to the following companies: + +:::: grid +::: grid-logo https://www.blazemeter.com/ +![BlazMeter logo](./blazemeter-logo.png) +::: +::: grid-logo https://octoperf.com/ +![OctoPerf logo](./octoperf-logo.png) +::: +::: grid-logo https://azure.microsoft.com/en-us/products/load-testing/ +![Azure Load Testing logo](./azure-logo.png) +::: +:::: + +[Discord server]:https://discord.gg/WNSn5hqmSd +[GitHub Issues]:https://github.com/abstracta/jmeter-dotnet-dsl/issues +[GitHub Discussions]:https://github.com/abstracta/jmeter-dotnet-dsl/discussions diff --git a/docs/support/octoperf-logo.png b/docs/support/octoperf-logo.png new file mode 100644 index 0000000..d39525e Binary files /dev/null and b/docs/support/octoperf-logo.png differ diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..f7bb735 Binary files /dev/null and b/logo.png differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e77bf6a --- /dev/null +++ b/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + us.abstracta.jmeter.dotnet + jmeter-dotnet-dsl-parent + 1.0-SNAPSHOT + pom + + This pom is only needed to be able to copy jmeter-java-dsl jars and dependencies with dependency:copy-dependencies maven plugin goal in child projects + + + 1.13 + + \ No newline at end of file