Merge pull request #19 from KevinMarquette/commonfiles !deploy

Add Commonfiles
This commit is contained in:
Kevin Marquette
2018-12-28 15:09:31 -08:00
committed by GitHub
41 changed files with 1354 additions and 188 deletions

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
/output/
/debug*.ps1
/temp*.ps1
# Most of these heavily cannibalized by Travis Drake from https://gist.github.com/kmorcinek/2710267
# C# VS build detritus
/.vs/
/*/[Bb]in/
/*/[Oo]bj/
**/packages/*
# Except this, this needs to be checked in when present
!**/packages/build/
# More C# / Visual Studio detritus
*.suo
*.user
*.sln.docstates
*.psess
*.vsp
*.vspx
project.lock.json
_UpgradeReport_Files/
UpgradeLog*.XML
UpgradeLog*.htm
# SASS Compiler cache
.sass-cache
# Mac OS stuff
.DS_Store*
Icon?
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/

26
.vscode/settings.json vendored
View File

@@ -1,4 +1,28 @@
// Place your settings in this file to overwrite default and user settings.
{
"powershell.codeFormatting.preset": "Allman"
//-------- Editor configuration --------
"editor.insertSpaces": true,
"editor.tabSize": 4,
//-------- Files configuration --------
"files.autoGuessEncoding": false,
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"search.exclude": {
"**/Tests/Data*": true
},
//-------- PowerShell configuration --------
"powershell.codeFormatting.alignPropertyValuePairs": true,
"powershell.codeFormatting.preset": "Allman",
"powershell.scriptAnalysis.settingsPath": "./ScriptAnalyzerSettings.psd1",
//-------- Language configuration --------
"[json]": {
"editor.tabSize": 2
},
"[xml]": {
"editor.tabSize": 2
}
}

95
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,95 @@
// Available variables which can be used inside of strings:
// ${workspaceRoot}: The root folder of the team
// ${file}: The current opened file
// ${relativeFile}: The current opened file relative to workspaceRoot
// ${fileBasename}: The current opened file's basename
// ${fileDirname}: The current opened file's dirname
// ${fileExtname}: The current opened file's extension
// ${cwd}: The current working directory of the spawned process
{
"version": "2.0.0",
"windows": {
"options": {
"shell": {
"executable": "powershell.exe",
"args": [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command" ]
}
}
},
"linux": {
"options": {
"shell": {
"executable": "/usr/bin/pwsh",
"args": [ "-NoProfile", "-Command" ]
}
}
},
"osx": {
"options": {
"shell": {
"executable": "/usr/local/bin/pwsh",
"args": [ "-NoProfile", "-Command" ]
}
}
},
"tasks": [
{
"label": "Default",
"type": "shell",
"problemMatcher": [ "$msCompile" ],
"group": {
"kind": "build",
"isDefault": true
},
"command": "Invoke-Build -Task Default -File './Module.build.ps1'"
},
{
"label": "Analyze",
"type": "shell",
"problemMatcher": [ "$msCompile" ],
"command": "Invoke-Build -Task Analyze -File './Module.build.ps1'"
},
{
"label": "Build",
"type": "shell",
"problemMatcher": [ "$msCompile" ],
"command": "Invoke-Build -Task Build -File './Module.build.ps1'"
},
{
"label": "Clean",
"type": "shell",
"problemMatcher": [ "$msCompile" ],
"command": "Invoke-Build -Task Clean -File './Module.build.ps1'"
},
{
"label": "Helpify",
"type": "shell",
"problemMatcher": [ "$msCompile" ],
"command": "Invoke-Build -Task Helpify -File './Module.build.ps1'"
},
{
"label": "Install",
"type": "shell",
"problemMatcher": [ "$msCompile" ],
"command": "Invoke-Build -Task Install -File './Module.build.ps1'"
},
{
"label": "Test",
"type": "shell",
"problemMatcher": [ "$msCompile" ],
"command": "Invoke-Build -Task Test -File './Module.build.ps1'"
},
{
"label": "Uninstall",
"type": "shell",
"problemMatcher": [ "$msCompile" ],
"command": "Invoke-Build -Task Uninstall -File './Module.build.ps1'"
},
{
"label": "?",
"type": "shell",
"problemMatcher": [],
"command": "Invoke-Build -Task ? -File './Module.build.ps1'"
}
]
}

View File

@@ -0,0 +1,17 @@
task Analyze {
$params = @{
IncludeDefaultRules = $true
Path = $ManifestPath
Settings = "$BuildRoot\ScriptAnalyzerSettings.psd1"
Severity = 'Warning'
}
"Analyzing $ManifestPath..."
$results = Invoke-ScriptAnalyzer @params
if ($results)
{
'One or more PSScriptAnalyzer errors/warnings were found.'
'Please investigate or add the required SuppressMessage attribute.'
$results | Format-Table -AutoSize
}
}

View File

@@ -0,0 +1,18 @@
taskx BuildManifest @{
Inputs = (Get-ChildItem -Path $Source -Recurse -File)
Outputs = $ManifestPath
Jobs = {
"Updating [$ManifestPath]..."
Copy-Item -Path "$Source\$ModuleName.psd1" -Destination $ManifestPath
$functions = Get-ChildItem -Path "$ModuleName\Public\*.ps1" -ErrorAction 'Ignore' |
Where-Object 'Name' -notmatch 'Tests'
if ($functions)
{
'Setting FunctionsToExport...'
Set-ModuleFunctions -Name $ManifestPath -FunctionsToExport $functions.BaseName
}
}
}

View File

@@ -0,0 +1,214 @@
# namespaces for Move-Statement
using namespace System.Collections.Generic
using namespace System.IO
using namespace System.Management.Automation
function Import-ClassOrder
{
[cmdletbinding()]
param($cName,$Map)
Write-Verbose "Checking on [$cName]"
if($Map.ContainsKey($cName) -and $Map[$cName].Imported -ne $true)
{
if($Map[$cName].Base)
{
Write-Verbose " Base class [$($Map[$cName].Base)]"
Import-ClassOrder $Map[$cName].Base $Map
}
$cPath = $Map[$cName].Path
Write-Verbose "Dot Sourcing [$cPath]"
$cPath
$Map[$cName].Imported = $true
}
}
# Temporarily added this here to be refactored/replaced by LDModuleBuilder Module
function Move-Statement
{
<#
.SYNOPSIS
Moves statements containing a specified token to the specified index in a file.
.DESCRIPTION
Move-Statement moves statements containing a specified token, to the specified index
in a file. This can be used when building a module to move any using directives and
#Requires statements to the top of a file.
.PARAMETER Path
Specifies the path to an item to get its contents.
.PARAMETER Type
Specifies the type of tokens to examine. Accepted values include "Comment" and "Keyword".
.PARAMETER Token
Specifies the contents to filter on when examining a supplied token.
.PARAMETER Index
Specifies the line to move a statement to. Each line in an item has a corresponding
index, starting from 0.
.EXAMPLE
Move-Statement -Path $Path -Type 'Comment', 'Keyword' -Token '#Requires', 'using' -Index 0
Moves any using directives or #Requires statements to the top of a file.
.NOTES
Copy/Paste from LDModuleBuilder
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory,
Position = 0,
ValueFromPipeline,
ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[ValidateScript({ Test-Path -Path $PSItem })]
[string] $Path,
[Parameter(Position = 1,
ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[ValidateSet('Comment', 'Keyword')]
[string[]] $Type = ('Comment', 'Keyword'),
[Parameter(Position = 2,
ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[string[]] $Token = ('#Requires', 'using'),
[Parameter(Position = 3,
ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[int] $Index = 0
)
process
{
try
{
$statements = [SortedSet[String]]::new(
[StringComparer]::InvariantCultureIgnoreCase
)
Write-Verbose -Message "Reading content from $Path..."
$content = [List[String]] ([File]::ReadLines($Path))
Write-Verbose -Message "Tokenizing content from $Path..."
$tokens = [PSParser]::Tokenize($content, [ref] $null)
$match = $Token -join '|'
Write-Verbose -Message 'Matching tokens...'
Write-Verbose -Message "Type = [$Type]; Token = [$Token]"
$keywords = $tokens.Where({
$PSItem.Type -in $Type -and
$PSItem.Content -imatch "^(?:$match)"
})
if (-not $keywords) {
Write-Verbose -Message 'No matching tokens found! Returning...'
return
}
$offset = 1
foreach ($keyword in $keywords)
{
$line = $keyword.StartLine - $offset
Write-Verbose -Message "Moving [$($content[$line])] to Index [$Index]..."
$null = $statements.Add($content[$line]),
$content.RemoveAt($line)
$offset++
}
[string[]] $comments, [string[]] $statements = $statements.Where({
$PSItem -match '^#'
}, 'Split')
foreach ($item in ($statements, $comments))
{
$content.Insert($Index, '')
$content.InsertRange($Index, $item)
}
if ($PSCmdlet.ShouldProcess($Path, $MyInvocation.MyCommand.Name))
{
Write-Verbose -Message "Writing content to $Path..."
[File]::WriteAllLines($Path, $content)
}
}
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
taskx BuildModule @{
Inputs = (Get-ChildItem -Path $Source -Recurse -Filter *.ps1)
Outputs = $ModulePath
Jobs = {
$sb = [Text.StringBuilder]::new()
$null = $sb.AppendLine('$Script:PSModuleRoot = $PSScriptRoot')
# Class importer
$root = Join-Path -Path $source -ChildPath 'Classes'
"Load classes from [$root]"
$classFiles = Get-ChildItem -Path $root -Filter '*.ps1' -Recurse |
Where-Object Name -notlike '*.Tests.ps1'
$classes = @{}
foreach($file in $classFiles)
{
$name = $file.BaseName
$classes[$name] = @{
Name = $name
Path = $file.FullName
}
$data = Get-Content $file.fullname
foreach($line in $data)
{
if($line -match "\s+($Name)\s*(:|requires)\s*(?<baseclass>\w*)")
{
$classes[$name].Base = $Matches.baseclass
}
}
}
$importOrder = foreach($className in $classes.Keys)
{
Import-ClassOrder $className $classes
}
foreach($class in $importOrder)
{
"Importing [$class]..."
$null = $sb.AppendLine("# .$class")
$null = $sb.AppendLine([IO.File]::ReadAllText($class))
}
foreach ($folder in ($Folders -ne 'Classes'))
{
if (Test-Path -Path "$Source\$folder")
{
$null = $sb.AppendLine("# Importing from [$Source\$folder]")
$files = Get-ChildItem -Path "$Source\$folder\*.ps1" |
Where-Object 'Name' -notlike '*.Tests.ps1'
foreach ($file in $files)
{
$name = $file.Fullname.Replace($buildroot, '')
"Importing [$($file.FullName)]..."
$null = $sb.AppendLine("# .$name")
$null = $sb.AppendLine([IO.File]::ReadAllText($file.FullName))
}
}
}
"Creating Module [$ModulePath]..."
$null = New-Item -Path (Split-path $ModulePath) -ItemType Directory -ErrorAction SilentlyContinue -Force
Set-Content -Path $ModulePath -Value $sb.ToString() -Encoding 'UTF8'
'Moving "#Requires" statements and "using" directives...'
Move-Statement -Path $ModulePath -Type 'Comment', 'Keyword' -Token '#Requires', 'using' -Index 0
}
}

12
BuildTasks/Clean.Task.ps1 Normal file
View File

@@ -0,0 +1,12 @@
task Clean {
if (Test-Path $Output)
{
"Cleaning Output files in [$Output]..."
$null = Get-ChildItem -Path $Output -File -Recurse |
Remove-Item -Force -ErrorAction 'Ignore'
"Cleaning Output directories in [$Output]..."
$null = Get-ChildItem -Path $Output -Directory -Recurse |
Remove-Item -Recurse -Force -ErrorAction 'Ignore'
}
}

View File

@@ -0,0 +1,14 @@
taskx Compile @{
If = (Get-ChildItem -Path $BuildRoot -Include *.csproj -Recurse)
Inputs = {
Get-ChildItem $BuildRoot -Recurse -File -Include *.cs
}
Outputs = "$Destination\bin\$ModuleName.dll"
Jobs = {
# This build command requires .Net Core
"Building Module"
$csproj = Get-ChildItem -Path $BuildRoot -Include *.csproj -Recurse
$folder = Split-Path $csproj
dotnet build $folder -c Release -o $Destination\bin
}
}

29
BuildTasks/Copy.Task.ps1 Normal file
View File

@@ -0,0 +1,29 @@
task Copy {
"Creating Directory [$Destination]..."
$null = New-Item -ItemType 'Directory' -Path $Destination -ErrorAction 'Ignore'
$files = Get-ChildItem -Path $Source -File |
Where-Object 'Name' -notmatch "$ModuleName\.ps[dm]1"
foreach ($file in $files)
{
'Creating [.{0}]...' -f $file.FullName.Replace($buildroot, '')
Copy-Item -Path $file.FullName -Destination $Destination -Force
}
$directories = Get-ChildItem -Path $Source -Directory |
Where-Object 'Name' -notin $Folders
foreach ($directory in $directories)
{
'Creating [.{0}]...' -f $directory.FullName.Replace($buildroot, '')
Copy-Item -Path $directory.FullName -Destination $Destination -Recurse -Force
}
$license = Join-Path -Path $buildroot -ChildPath 'LICENSE'
if ( Test-Path -Path $license -PathType Leaf )
{
Copy-Item -Path $license -Destination $Destination
}
}

View File

@@ -0,0 +1,23 @@
task GenerateHelp {
if (-not(Get-ChildItem -Path $DocsPath -Filter '*.md' -Recurse -ErrorAction 'Ignore'))
{
"No Markdown help files to process. Skipping help file generation..."
return
}
$locales = (Get-ChildItem -Path $DocsPath -Directory).Name
foreach ($locale in $locales)
{
$params = @{
ErrorAction = 'SilentlyContinue'
Force = $true
OutputPath = "$Destination\en-US"
Path = "$DocsPath\en-US"
}
# Generate the module's primary MAML help file.
"Creating new External help for [$ModuleName]..."
$null = New-ExternalHelp @params
}
}

View File

@@ -0,0 +1,41 @@
task GenerateMarkdown {
$module = Import-Module -FullyQualifiedName $ManifestPath -Force -PassThru
try
{
if ($module.ExportedFunctions.Count -eq 0)
{
'No functions have been exported for this module. Skipping Markdown generation...'
return
}
if (Get-ChildItem -Path $DocsPath -Filter '*.md' -Recurse)
{
$items = Get-ChildItem -Path $DocsPath -Directory -Recurse
foreach ($item in $items)
{
"Updating Markdown help in [$($item.BaseName)]..."
$null = Update-MarkdownHelp -Path $item.FullName -AlphabeticParamsOrder
}
}
$params = @{
AlphabeticParamsOrder = $true
ErrorAction = 'SilentlyContinue'
Locale = 'en-US'
Module = $ModuleName
OutputFolder = "$DocsPath\en-US"
WithModulePage = $true
}
# ErrorAction is set to SilentlyContinue so this
# command will not overwrite an existing Markdown file.
"Creating new Markdown help for [$ModuleName]..."
$null = New-MarkdownHelp @params
}
finally
{
Remove-Module -Name $ModuleName -Force
}
}

View File

@@ -0,0 +1,4 @@
task ImportDevModule {
ImportModule -Path "$Source\$ModuleName.psd1" -Force
}

View File

@@ -0,0 +1,33 @@
function ImportModule
{
param(
[string]$path,
[switch]$PassThru
)
if (-not(Test-Path -Path $path))
{
"Cannot find [$path]."
Write-Error -Message "Could not find module manifest [$path]"
}
else
{
$file = Get-Item $path
$name = $file.BaseName
$loaded = Get-Module -Name $name -All -ErrorAction Ignore
if ($loaded)
{
"Unloading Module [$name] from a previous import..."
$loaded | Remove-Module -Force
}
"Importing Module [$name] from [$($file.fullname)]..."
Import-Module -Name $file.fullname -Force -PassThru:$PassThru
}
}
task ImportModule {
ImportModule -Path $ManifestPath
}

View File

@@ -0,0 +1,21 @@
task Install Uninstall, {
$version = [version] (Get-Metadata -Path $manifestPath -PropertyName 'ModuleVersion')
$path = $env:PSModulePath.Split(';').Where({
$_ -like 'C:\Users\*'
}, 'First', 1)
if ($path -and (Test-Path -Path $path))
{
"Using [$path] as base path..."
$path = Join-Path -Path $path -ChildPath $ModuleName
$path = Join-Path -Path $path -ChildPath $version
"Creating directory at [$path]..."
New-Item -Path $path -ItemType 'Directory' -Force -ErrorAction 'Ignore'
"Copying items from [$Destination] to [$path]..."
Copy-Item -Path "$Destination\*" -Destination $path -Recurse -Force
}
}

View File

@@ -0,0 +1,31 @@
Write-Verbose "Initializing build variables" -Verbose
Write-Verbose " Existing BuildRoot [$BuildRoot]" -Verbose
$Script:DocsPath = Join-Path -Path $BuildRoot -ChildPath 'Docs'
Write-Verbose " DocsPath [$DocsPath]" -Verbose
$Script:Output = Join-Path -Path $BuildRoot -ChildPath 'Output'
Write-Verbose " Output [$Output]" -Verbose
$Script:Source = Join-Path -Path $BuildRoot -ChildPath $ModuleName
Write-Verbose " Source [$Source]" -Verbose
$Script:Destination = Join-Path -Path $Output -ChildPath $ModuleName
Write-Verbose " Destination [$Destination]" -Verbose
$Script:ManifestPath = Join-Path -Path $Destination -ChildPath "$ModuleName.psd1"
Write-Verbose " ManifestPath [$ManifestPath]" -Verbose
$Script:ModulePath = Join-Path -Path $Destination -ChildPath "$ModuleName.psm1"
Write-Verbose " ModulePath [$ModulePath]" -Verbose
$Script:Folders = 'Classes', 'Includes', 'Internal', 'Private', 'Public', 'Resources'
Write-Verbose " Folders [$Folders]" -Verbose
$Script:TestFile = "$BuildRoot\Output\TestResults_PS$PSVersion`_$TimeStamp.xml"
Write-Verbose " TestFile [$TestFile]" -Verbose
$Script:PSRepository = 'PSGallery'
Write-Verbose " PSRepository [$TestFile]" -Verbose
function taskx($Name, $Parameters) { task $Name @Parameters -Source $MyInvocation }

View File

@@ -0,0 +1,34 @@
task Pester {
$requiredPercent = $Script:CodeCoveragePercent
$params = @{
OutputFile = $testFile
OutputFormat = 'NUnitXml'
PassThru = $true
Path = 'Tests'
Show = 'Failed', 'Fails', 'Summary'
Tag = 'Build'
}
if($requiredPercent -gt 0.00)
{
$params['CodeCoverage'] = 'Output\*\*.psm1'
$params['CodeCoverageOutputFile'] = 'Output\codecoverage.xml'
}
$results = Invoke-Pester @params
if ($results.FailedCount -gt 0)
{
Write-Error -Message "Failed [$($results.FailedCount)] Pester tests."
}
if($results.codecoverage.NumberOfCommandsAnalyzed -gt 0)
{
$codeCoverage = $results.codecoverage.NumberOfCommandsExecuted / $results.codecoverage.NumberOfCommandsAnalyzed
if($codeCoverage -lt $requiredPercent)
{
Write-Error ("Failed Code Coverage [{0:P}] below {1:P}" -f $codeCoverage,$requiredPercent)
}
}
}

View File

@@ -0,0 +1,31 @@
task PublishModule {
if ( $ENV:BHBuildSystem -ne 'Unknown' -and
$ENV:BHBranchName -eq "master" -and
-not [string]::IsNullOrWhiteSpace($ENV:nugetapikey))
{
$publishModuleSplat = @{
Path = $Destination
NuGetApiKey = $ENV:nugetapikey
Verbose = $true
Force = $true
Repository = $PSRepository
ErrorAction = 'Stop'
}
"Files in module output:"
Get-ChildItem $Destination -Recurse -File |
Select-Object -Expand FullName
"Publishing [$Destination] to [$PSRepository]"
Publish-Module @publishModuleSplat
}
else
{
"Skipping deployment: To deploy, ensure that...`n" +
"`t* You are in a known build system (Current: $ENV:BHBuildSystem)`n" +
"`t* You are committing to the master branch (Current: $ENV:BHBranchName) `n" +
"`t* The repository APIKey is defined in `$ENV:nugetapikey (Current: $(![string]::IsNullOrWhiteSpace($ENV:nugetapikey))) `n" +
"`t* This is not a pull request"
}
}

View File

@@ -0,0 +1,7 @@
task PublishVersion {
[version] $sourceVersion = (Get-Metadata -Path $manifestPath -PropertyName 'ModuleVersion')
"##vso[build.updatebuildnumber]$sourceVersion"
# Do the same for appveyor
# https://www.appveyor.com/docs/build-worker-api/#update-build-details
}

View File

@@ -0,0 +1,125 @@
function GetModulePublicInterfaceMap
{
param($Path)
$module = ImportModule -Path $Path -PassThru
$exportedCommands = @(
$module.ExportedFunctions.values
$module.ExportedCmdlets.values
$module.ExportedAliases.values
)
foreach($command in $exportedCommands)
{
foreach ($parameter in $command.Parameters.Keys)
{
if($false -eq $command.Parameters[$parameter].IsDynamic)
{
'{0}:{1}' -f $command.Name, $command.Parameters[$parameter].Name
foreach ($alias in $command.Parameters[$parameter].Aliases)
{
'{0}:{1}' -f $command.Name, $alias
}
}
}
}
}
task SetVersion {
$version = [version]"0.1.0"
$publishedModule = $null
$bumpVersionType = 'Patch'
$versionStamp = (git rev-parse origin/master) + (git rev-parse head)
"Load current version"
[version] $sourceVersion = (Get-Metadata -Path $manifestPath -PropertyName 'ModuleVersion')
" Source version [$sourceVersion]"
$downloadFolder = Join-Path -Path $output downloads
$null = New-Item -ItemType Directory -Path $downloadFolder -Force -ErrorAction Ignore
$versionFile = Join-Path $downloadFolder versionfile
if(Test-Path $versionFile)
{
$versionFileData = Get-Content $versionFile -raw
if($versionFileData -eq $versionStamp)
{
continue
}
}
"Checking for published version"
$publishedModule = Find-Module -Name $ModuleName -ErrorAction 'Ignore' |
Sort-Object -Property {[version]$_.Version} -Descending |
Select -First 1
if($null -ne $publishedModule)
{
[version] $publishedVersion = $publishedModule.Version
" Published version [$publishedVersion]"
$version = $publishedVersion
"Downloading published module to check for breaking changes"
$publishedModule | Save-Module -Path $downloadFolder
[System.Collections.Generic.HashSet[string]] $publishedInterface = @(GetModulePublicInterfaceMap -Path (Join-Path $downloadFolder $ModuleName))
[System.Collections.Generic.HashSet[string]] $buildInterface = @(GetModulePublicInterfaceMap -Path $ManifestPath)
if (-not $publishedInterface.IsSubsetOf($buildInterface))
{
$bumpVersionType = 'Major'
}
elseif ($publishedInterface.count -ne $buildInterface.count)
{
$bumpVersionType = 'Minor'
}
}
if ($version -lt ([version] '1.0.0'))
{
"Module is still in beta; don't bump major version."
if ($bumpVersionType -eq 'Major')
{
$bumpVersionType = 'Minor'
}
else
{
$bumpVersionType = 'Patch'
}
}
" Steping version [$bumpVersionType]"
$version = [version] (Step-Version -Version $version -Type $bumpVersionType)
" Comparing to source version [$sourceVersion]"
if($sourceVersion -gt $version)
{
" Using existing version"
$version = $sourceVersion
}
if ( -not [string]::IsNullOrEmpty( $env:Build_BuildID ) )
{
$build = $env:Build_BuildID
$version = [version]::new($version.Major, $version.Minor, $version.Build, $build)
}
elseif ( -not [string]::IsNullOrEmpty( $env:APPVEYOR_BUILD_ID ) )
{
$build = $env:APPVEYOR_BUILD_ID
$version = [version]::new($version.Major, $version.Minor, $version.Build, $build)
}
" Setting version [$version]"
Update-Metadata -Path $ManifestPath -PropertyName 'ModuleVersion' -Value $version
(Get-Content -Path $ManifestPath -Raw -Encoding UTF8) |
ForEach-Object {$_.TrimEnd()} |
Set-Content -Path $ManifestPath -Encoding UTF8
Set-Content -Path $versionFile -Value $versionStamp -NoNewline -Encoding UTF8
if(Test-Path $BuildRoot\fingerprint)
{
Remove-Item $BuildRoot\fingerprint
}
}

View File

@@ -0,0 +1,28 @@
task Uninstall {
'Unloading Modules...'
Get-Module -Name $ModuleName -ErrorAction 'Ignore' | Remove-Module -Force
'Uninstalling Module packages...'
$modules = Get-Module $ModuleName -ErrorAction 'Ignore' -ListAvailable
foreach ($module in $modules)
{
Uninstall-Module -Name $module.Name -RequiredVersion $module.Version -ErrorAction 'Ignore'
}
'Cleaning up manually installed Modules...'
$path = $env:PSModulePath.Split(';').Where({
$_ -like 'C:\Users\*'
}, 'First', 1)
$path = Join-Path -Path $path -ChildPath $ModuleName
if ($path -and (Test-Path -Path $path))
{
'Removing files... (This may fail if any DLLs are in use.)'
Get-ChildItem -Path $path -File -Recurse |
Remove-Item -Force | ForEach-Object 'FullName'
'Removing folders... (This may fail if any DLLs are in use.)'
Remove-Item $path -Recurse -Force
}
}

View File

@@ -0,0 +1,6 @@
task UpdateSource {
Copy-Item -Path $ManifestPath -Destination "$Source\$ModuleName.psd1"
$content = Get-Content -Path "$Source\$ModuleName.psd1" -Raw -Encoding UTF8
$content.Trim() | Set-Content -Path "$Source\$ModuleName.psd1" -Encoding UTF8
}

View File

@@ -1,4 +1,4 @@
# Module manifest for module 'chronometer'
# Module manifest for module 'chronometer'
# Generated by: Kevin Marquette
# Generated on: 2/2/2017
@@ -8,7 +8,7 @@
RootModule = 'chronometer.psm1'
# Version number of this module.
ModuleVersion = '0.5.2'
ModuleVersion = '1.0.0'
# ID used to uniquely identify this module
GUID = 'f3719c3c-008a-4b25-b94d-fc9f587f62dd'
@@ -17,7 +17,7 @@ GUID = 'f3719c3c-008a-4b25-b94d-fc9f587f62dd'
Author = 'Kevin Marquette'
# Copyright statement for this module
Copyright = '(c) 2017 Kevin Marquette. All rights reserved.'
Copyright = '(c) 2019 Kevin Marquette. All rights reserved.'
# Description of the functionality provided by this module
Description = 'Performs a line by line measurement of execution times for scripts'
@@ -26,7 +26,7 @@ Description = 'Performs a line by line measurement of execution times for script
PowerShellVersion = '5.0'
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @('Get-Chronometer','Format-Chronometer')
FunctionsToExport = @('Format-Chronometer','Get-Chronometer')
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()
@@ -55,4 +55,3 @@ PrivateData = @{
} # End of PrivateData hashtable
}

View File

@@ -0,0 +1,126 @@
---
external help file: chronometer-help.xml
Module Name: chronometer
online version:
schema: 2.0.0
---
# Format-Chronometer
## SYNOPSIS
## SYNTAX
### Script (Default)
```
Format-Chronometer [-InputObject <MonitoredScript[]>] [-WarningAt <Int32>] [-ErrorAt <Int32>] [-ShowAll]
[<CommonParameters>]
```
### Line
```
Format-Chronometer [-Line <ScriptLine[]>] [-WarningAt <Int32>] [-ErrorAt <Int32>] [-ShowAll]
[<CommonParameters>]
```
## DESCRIPTION
Generates a report from a Chronometer
## EXAMPLES
### EXAMPLE 1
```
$script = ls C:\workspace\PSGraph\PSGraph -Recurse -Filter *.ps1
```
$resultes = Get-Chronometer -Path $script.fullname -ScriptBlock {Invoke-Pester C:\workspace\PSGraph}
$results | Format-Chronometer -WarnAt 20 -ErrorAt 200
## PARAMETERS
### -ErrorAt
If the average time of a comamand is more than this, the output is red
```yaml
Type: Int32
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: 200
Accept pipeline input: False
Accept wildcard characters: False
```
### -InputObject
This is a MonitoredScript object from Get-Chronometer
```yaml
Type: MonitoredScript[]
Parameter Sets: Script
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: True (ByValue)
Accept wildcard characters: False
```
### -Line
This is a ScriptLine object from a MonitoredScript object
```yaml
Type: ScriptLine[]
Parameter Sets: Line
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: True (ByValue)
Accept wildcard characters: False
```
### -ShowAll
Forces the report to show scripts with no execution time
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```
### -WarningAt
If the average time of a command is more than this, the output is yellow
```yaml
Type: Int32
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: 20
Accept pipeline input: False
Accept wildcard characters: False
```
### CommonParameters
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable.
For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216).
## INPUTS
## OUTPUTS
## NOTES
## RELATED LINKS

View File

@@ -0,0 +1,88 @@
---
external help file: chronometer-help.xml
Module Name: chronometer
online version:
schema: 2.0.0
---
# Get-Chronometer
## SYNOPSIS
## SYNTAX
```
Get-Chronometer [-Path <Object[]>] [-LineNumber <Int32[]>] [[-ScriptBlock] <ScriptBlock>] [<CommonParameters>]
```
## DESCRIPTION
Loads a script and then tracks the line by line execution times
## EXAMPLES
### EXAMPLE 1
```
Get-Chronometer -Path .\example.ps1 -Script {
```
.\example.ps1
}
## PARAMETERS
### -LineNumber
Line numbers within the script file to measure
```yaml
Type: Int32[]
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -Path
Script file to measure execution times on
```yaml
Type: Object[]
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: True (ByValue)
Accept wildcard characters: False
```
### -ScriptBlock
The script to start the scrupt or execute other commands
```yaml
Type: ScriptBlock
Parameter Sets: (All)
Aliases: Script, CommandScript
Required: False
Position: 1
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### CommonParameters
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable.
For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216).
## INPUTS
## OUTPUTS
## NOTES
## RELATED LINKS

19
Docs/en-US/chronometer.md Normal file
View File

@@ -0,0 +1,19 @@
---
Module Name: chronometer
Module Guid: f3719c3c-008a-4b25-b94d-fc9f587f62dd
Download Help Link: {{Please enter FwLink manually}}
Help Version: {{Please enter version of help manually (X.X.X.X) format}}
Locale: en-US
---
# chronometer Module
## Description
{{Manually Enter Description Here}}
## chronometer Cmdlets
### [Format-Chronometer](Format-Chronometer.md)
{{Manually Enter Format-Chronometer Description Here}}
### [Get-Chronometer](Get-Chronometer.md)
{{Manually Enter Get-Chronometer Description Here}}

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017 Kevin Marquette
Copyright (c) 2019 Kevin Marquette
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

15
Module.build.ps1 Normal file
View File

@@ -0,0 +1,15 @@
$Script:ModuleName = Get-ChildItem .\*\*.psm1 | Select-object -ExpandProperty BaseName
$Script:CodeCoveragePercent = 0.0 # 0 to 1
. $psscriptroot\BuildTasks\InvokeBuildInit.ps1
task Default Build, Helpify, Test, UpdateSource
task Build Copy, Compile, BuildModule, BuildManifest, SetVersion
task Helpify GenerateMarkdown, GenerateHelp
task Test Build, ImportModule, Pester
task Publish Build, PublishVersion, Helpify, Test, PublishModule
task TFS Clean, Build, PublishVersion, Helpify, Test
task DevTest ImportDevModule, Pester
Write-Host 'Import common tasks'
Get-ChildItem -Path $buildroot\BuildTasks\*.Task.ps1 |
ForEach-Object {Write-Host $_.FullName;. $_.FullName}

View File

@@ -0,0 +1,34 @@
@{
# Use Severity when you want to limit the generated diagnostic records to a
# subset of: Error, Warning and Information.
# Uncomment the following line if you only want Errors and Warnings but
# not Information diagnostic records.
Severity = @('Error','Warning')
# Use IncludeRules when you want to run only a subset of the default rule set.
#IncludeRules = @('PSAvoidDefaultValueSwitchParameter',
# 'PSMisleadingBacktick',
# 'PSMissingModuleManifestField',
# 'PSReservedCmdletChar',
# 'PSReservedParams',
# 'PSShouldProcess',
# 'PSUseApprovedVerbs',
# 'PSUseDeclaredVarsMoreThanAssigments')
# Use ExcludeRules when you want to run most of the default set of rules except
# for a few rules you wish to "exclude". Note: if a rule is in both IncludeRules
# and ExcludeRules, the rule will be excluded.
ExcludeRules = @('PSUseToExportFieldsInManifest','PSMissingModuleManifestField')
# You can use the following entry to supply parameters to rules that take parameters.
# For instance, the PSAvoidUsingCmdletAliases rule takes a whitelist for aliases you
# want to allow.
Rules = @{
# Do not flag 'cd' alias.
PSAvoidUsingCmdletAliases = @{Whitelist = @('Where','Select')}
# Check if your script uses cmdlets that are compatible on PowerShell Core,
# version 6.0.0-alpha, on Linux.
# PSUseCompatibleCmdlets = @{Compatibility = @("core-6.0.0-alpha-linux")}
}
}

View File

@@ -0,0 +1,16 @@
InModuleScope Chronometer {
Describe "Class: MonitoredScript" -Tag Build {
It "Creates an object" {
{[MonitoredScript]::New()} | Should Not Throw
}
It "SetScript()" {
$monitor = [MonitoredScript]::New()
{$monitor.SetScript("$PSScriptRoot\..\..\scratchfiles\example.ps1")} | Should Not Throw
}
}
}

View File

@@ -0,0 +1,16 @@
InModuleScope Chronometer {
Describe "Class: ScriptLine" -Tag Build {
It "Creates an Object" {
{[ScriptLine]::New()} | Should Not Throw
}
It "ToString()" {
{[ScriptLine]::New().toString()} | Should Not Throw
}
It "Creates an Object" {
{[ScriptLine]::New().AddExecutionTime(1)} | Should Not Throw
}
}
}

View File

@@ -0,0 +1,13 @@
InModuleScope Chronometer {
Describe "Class: ScriptProfiler" -Tag Build {
It "Creates an Object" {
{[ScriptProfiler]::New()} | Should Not Throw
}
It "Start()" {
{[ScriptProfiler]::Start()} | Should Not Throw
}
}
}

View File

@@ -0,0 +1,11 @@
Describe "Function: Format-Chronometer" -Tag Build {
It "Does not throw" {
{$null | Format-Chronometer } | Should Not Throw
}
It "Can process a result object without throwing" {
$results = Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {. "$PSScriptRoot\..\ScratchFiles\example.ps1"}
$results | Format-Chronometer *>&1 | Should Not BeNullOrEmpty
}
}

View File

@@ -0,0 +1,23 @@
Describe "Function: Get-Chronometer" -Tag Build {
It "Does not throw" {
# Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"}
{Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {"Test"} } | Should Not Throw
}
It "Executes a script and gives results" {
# Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"}
$results = Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {. "$PSScriptRoot\..\ScratchFiles\example.ps1"}
$results | Should Not BeNullOrEmpty
}
It "Executes a script with line numbers and gives results" {
# Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"}
$params = @{
Path = "$PSScriptRoot\..\ScratchFiles\example.ps1"
Script = {. "$PSScriptRoot\..\ScratchFiles\example.ps1"}
LineNumber = 2,3,5,6
}
$results = Get-Chronometer @params
$results | Should Not BeNullOrEmpty
}
}

View File

@@ -1,30 +0,0 @@
$projectRoot = Resolve-Path "$PSScriptRoot\.."
$moduleRoot = Split-Path (Resolve-Path "$projectRoot\*\*.psm1")
$moduleName = Split-Path $moduleRoot -Leaf
Describe "Help tests for $moduleName" -Tags Build {
Import-Module (Join-Path $moduleRoot "$moduleName.psm1") -force
$functions = Get-Command -Module $moduleName
$help = $functions | %{Get-Help $_.name}
foreach($node in $help)
{
Context $node.name {
it "has a description" {
$node.description | Should Not BeNullOrEmpty
}
it "has an example" {
$node.examples | Should Not BeNullOrEmpty
}
foreach($parameter in $node.parameters.parameter)
{
it "parameter $($parameter.name) has a description" {
$parameter.Description.text | Should Not BeNullOrEmpty
}
}
}
}
}

View File

@@ -1,51 +0,0 @@
$projectRoot = Resolve-Path "$PSScriptRoot\.."
$moduleRoot = Split-Path (Resolve-Path "$projectRoot\*\*.psd1")
$moduleName = Split-Path $moduleRoot -Leaf
Describe "General project validation: $moduleName" -Tags Build {
Context "Valid Powershell" {
$scripts = Get-ChildItem $projectRoot -Include *.ps1,*.psm1,*.psd1 -Recurse | where fullname -notmatch 'classes'
# TestCases are splatted to the script so we need hashtables
$testCase = $scripts | Foreach-Object{@{file=$_}}
It "Script <file> should be valid powershell" -TestCases $testCase {
param($file)
$file.fullname | Should Exist
$contents = Get-Content -Path $file.fullname -ErrorAction Stop
$errors = $null
$null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors)
$errors.Count | Should Be 0
}
It "Classes are valid" {
$classes = Get-ChildItem $projectRoot -Include *.ps1,*.psm1,*.psd1 -Recurse | where fullname -match 'classes'
# Must be imported togehter incase they depend on each other
$contents = Get-Content -Path $classes.FullName | Out-String
$errors = $null
$null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors)
$errors.Count | Should Be 0
}
}
Context "ScriptAnalyzer" {
$scripts = Get-ChildItem $moduleRoot -Include *.ps1,*.psm1,*.psd1 -Recurse | where fullname -notmatch 'classes'
$testCase = $scripts | Foreach-Object{@{file=$_}}
it "Script <file> should pass ScriptAnalyzer rules" -TestCases $testCase {
param($file)
$file.fullname | Should Exist
Invoke-ScriptAnalyzer $file| Should BeNullOrEmpty
}
}
It "Module '$moduleName' can import cleanly" {
{Import-Module (Join-Path $moduleRoot "$moduleName.psm1") -force } | Should Not Throw
}
}

View File

@@ -0,0 +1,33 @@
$Script:ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
$Script:ModuleName = $Script:ModuleName = Get-ChildItem $ModuleRoot\*\*.psm1 | Select-object -ExpandProperty BaseName
Describe "Public commands have comment-based or external help" -Tags 'Build' {
$functions = Get-Command -Module $ModuleName
$help = foreach ($function in $functions) {
Get-Help -Name $function.Name
}
foreach ($node in $help)
{
Context $node.Name {
It "Should have a Description or Synopsis" {
($node.Description + $node.Synopsis) | Should Not BeNullOrEmpty
}
It "Should have an Example" {
$node.Examples | Should Not BeNullOrEmpty
$node.Examples | Out-String | Should -Match ($node.Name)
}
foreach ($parameter in $node.Parameters.Parameter)
{
if ($parameter -notmatch 'WhatIf|Confirm')
{
It "Should have a Description for Parameter [$($parameter.Name)]" {
$parameter.Description.Text | Should Not BeNullOrEmpty
}
}
}
}
}
}

View File

@@ -0,0 +1,45 @@
$Script:ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
$Script:ModuleName = $Script:ModuleName = Get-ChildItem $ModuleRoot\*\*.psm1 | Select-object -ExpandProperty BaseName
$Script:SourceRoot = Join-Path -Path $ModuleRoot -ChildPath $ModuleName
Describe "All commands pass PSScriptAnalyzer rules" -Tag 'Build' {
$rules = "$ModuleRoot\ScriptAnalyzerSettings.psd1"
$scripts = Get-ChildItem -Path $SourceRoot -Include '*.ps1', '*.psm1', '*.psd1' -Recurse |
Where-Object FullName -notmatch 'Classes'
foreach ($script in $scripts)
{
Context $script.FullName {
$results = Invoke-ScriptAnalyzer -Path $script.FullName -Settings $rules
if ($results)
{
foreach ($rule in $results)
{
It $rule.RuleName {
$message = "{0} Line {1}: {2}" -f $rule.Severity, $rule.Line, $rule.Message
$message | Should Be ""
}
}
}
else
{
It "Should not fail any rules" {
$results | Should BeNullOrEmpty
}
}
}
}
}
Describe "Public commands have Pester tests" -Tag 'Build' {
$commands = Get-Command -Module $ModuleName
foreach ($command in $commands.Name)
{
$file = Get-ChildItem -Path "$ModuleRoot\Tests" -Include "$command.Tests.ps1" -Recurse
It "Should have a Pester test for [$command]" {
$file.FullName | Should Not BeNullOrEmpty
}
}
}

View File

@@ -1,83 +0,0 @@
$projectRoot = Resolve-Path "$PSScriptRoot\.."
$moduleRoot = Split-Path (Resolve-Path "$projectRoot\*\*.psd1")
$moduleName = Split-Path $moduleRoot -Leaf
Describe "Basic unit tests" -Tags Build {
Import-Module (Join-Path $moduleRoot "$moduleName.psm1") -force
Context "Function: Get-Chronometer" {
it "Does not throw" {
# Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"}
{Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {"Test"} } | Should Not Throw
}
it "Executes a script and gives results" {
# Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"}
$results = Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {. "$PSScriptRoot\..\ScratchFiles\example.ps1"}
$results | Should Not BeNullOrEmpty
}
it "Executes a script with linenumbers and gives results" {
# Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"}
$params = @{
Path = "$PSScriptRoot\..\ScratchFiles\example.ps1"
Script = {. "$PSScriptRoot\..\ScratchFiles\example.ps1"}
LineNumber = 2,3,5,6
}
$results = Get-Chronometer @params
$results | Should Not BeNullOrEmpty
}
}
Context "Function: Format-Chronometer" {
it "Does not throw" {
{$null | Format-Chronometer } | Should Not Throw
}
it "Can process a result object without throwing" {
$results = Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {. "$PSScriptRoot\..\ScratchFiles\example.ps1"}
$results | Format-Chronometer *>&1 | Should Not BeNullOrEmpty
}
}
InModuleScope $moduleName {
Context "Class: ScriptLine" {
it "Creates an Object" {
{[ScriptLine]::New()} | Should Not Throw
}
it "ToString()" {
{[ScriptLine]::New().toString()} | Should Not Throw
}
it "Creates an Object" {
{[ScriptLine]::New().AddExecutionTime(1)} | Should Not Throw
}
}
Context "Class: ScriptProfiler" {
it "Creates an Object" {
{[ScriptProfiler]::New()} | Should Not Throw
}
it "Start()" {
{[ScriptProfiler]::Start()} | Should Not Throw
}
}
Context "Class: MonitoredScript" {
it "Creates an object" {
{[MonitoredScript]::New()} | Should Not Throw
}
it "SetScript()" {
pushd $projectRoot
$monitor = [MonitoredScript]::New()
{$monitor.SetScript(".\scratchfiles\example.ps1")} | Should Not Throw
popd
}
}
}
}

25
azure-pipelines.yml Normal file
View File

@@ -0,0 +1,25 @@
# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
#resources:
#- repo: self
# clean: true
# fetchDepth: 1
trigger:
batch: true
branches:
include:
- master
pool:
vmImage: 'Ubuntu 16.04'
steps:
- script: pwsh -File build.ps1 Publish
displayName: 'Build and Publish Module'
env:
nugetapikey: $(nugetapikey)

View File

@@ -1,21 +1,38 @@
<#
.Description
Installs and loads all the required modules for the build.
.Author
Warren F. (RamblingCookieMonster)
#>
[CmdletBinding()]
[cmdletbinding()]
param($Task = 'Default')
# Grab nuget bits, install modules, set build variables, start build.
Get-PackageProvider -Name NuGet -ForceBootstrap | Out-Null
$Script:Modules = @(
'BuildHelpers',
'InvokeBuild',
'Pester',
'platyPS',
'PSScriptAnalyzer',
'DependsOn'
)
Install-Module Psake, PSDeploy, BuildHelpers, PSScriptAnalyzer -force
Install-Module Pester -Force -SkipPublisherCheck
Import-Module Psake, BuildHelpers, PSScriptAnalyzer
$Script:ModuleInstallScope = 'CurrentUser'
'Starting build...'
'Installing module dependencies...'
Get-PackageProvider -Name 'NuGet' -ForceBootstrap | Out-Null
Install-Module -Name $Script:Modules -Scope $Script:ModuleInstallScope -Force -SkipPublisherCheck
Set-BuildEnvironment
Get-ChildItem Env:BH*
Get-ChildItem Env:APPVEYOR*
Invoke-psake -buildFile .\psake.ps1 -taskList $Task -nologo
exit ( [int]( -not $psake.build_success ) )
$Error.Clear()
'Invoking build...'
Invoke-Build $Task -Result 'Result'
if ($Result.Error)
{
$Error[-1].ScriptStackTrace | Out-String
exit 1
}
exit 0