Convert RESTler bug buckets to Postman collections (#168)

Co-authored-by: stas <statis@microsoft.com>
This commit is contained in:
Stas 2021-03-24 14:50:05 -07:00 коммит произвёл GitHub
Родитель b5d1ff4c07
Коммит 6336c84ff5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 559 добавлений и 19 удалений

Просмотреть файл

@ -37,6 +37,12 @@ stages:
vmImage: 'ubuntu-latest'
steps:
- template: steps/result-analyzer.yml
- job: RESTler2Postman
pool:
vmImage: 'ubuntu-latest'
steps:
- template: steps/restler2postman.yml
- job: AzureAuthUtility
pool:

Просмотреть файл

@ -0,0 +1,24 @@
steps:
- task: NuGetToolInstaller@1
displayName: 'Use NuGet 5.8'
inputs:
versionSpec: 5.8
- task: NuGetCommand@2
displayName: 'NuGet restore'
inputs:
restoreSolution: '**\RESTlerAgent.sln'
- task: DotNetCoreCLI@2
displayName: 'RESTler To Postman'
inputs:
command: publish
publishWebProjects: false
projects: src/Agent/RESTler2Postman/RESTler2Postman.fsproj
arguments: '-c release /p:version=$(versionNumber)'
zipAfterPublish: false
- task: PublishPipelineArtifact@1
inputs:
targetPath: src/Agent/RESTler2Postman/bin/release/net5.0/publish/
artifactName: RESTler2Postman

Просмотреть файл

@ -16,6 +16,12 @@ steps:
artifact: RaftResultAnalyzer
path: $(Build.SourcesDirectory)/artifacts/RaftResultAnalyzer
- task: DownloadPipelineArtifact@2
displayName: 'Download Local pipeline artifact RESTler2Postman'
inputs:
artifact: RESTler2Postman
path: $(Build.SourcesDirectory)/artifacts/RESTler2Postman
- ${{ if eq(parameters.BuildArtifactsLocation, 'production') }}:
- task: DownloadPipelineArtifact@2
displayName: 'Download Production pipeline artifact RestlerAgent'
@ -40,6 +46,17 @@ steps:
path: $(Build.SourcesDirectory)/artifacts/RaftResultAnalyzer
runVersion: 'latest'
- task: DownloadPipelineArtifact@2
displayName: 'Download Production pipeline artifact RESTler2Postman'
inputs:
source: 'specific'
project: 'raft'
pipeline: $(build-production-pipeline-id)
artifact: RESTler2Postman
path: $(Build.SourcesDirectory)/artifacts/RESTler2Postman
runVersion: 'latest'
# This authenticates against the service connection which is needed to pull the restler image
# from the repository.
- task: Docker@2

Просмотреть файл

@ -6,14 +6,39 @@
"testTasks": {
"targetConfiguration" : {
"endpoint" : "https://petstore.swagger.io",
"apiSpecifications": [
"https://petstore.swagger.io/v2/swagger.json"
]
},
"tasks": [
{
"targetConfiguration" : {
"endpoint" : "https://petstore3.swagger.io",
"apiSpecifications": [
"https://petstore3.swagger.io/api/v3/openapi.json"
]
},
"toolName": "RESTler",
"outputFolder": "restler-run",
"outputFolder": "restler-pestorev3",
"toolConfiguration": {
"tasks": [
{"task": "Compile"},
{"task" : "Test"},
{
"task" : "Fuzz",
"runConfiguration" : {
"Duration" : "00:10:00"
}
}
]
}
},
{
"toolName": "RESTler",
"outputFolder": "restler-petstorev2",
"toolConfiguration": {
"tasks": [
{"task": "Compile"},

Просмотреть файл

@ -1,3 +1,4 @@
FROM mcr.microsoft.com/restlerfuzzer/restler:v7.3.0
COPY RestlerAgent /raft/agent
COPY RaftResultAnalyzer /raft/result-analyzer
COPY RESTler2Postman /raft/restler2postman

Просмотреть файл

@ -0,0 +1,407 @@
// Learn more about F# at http://docs.microsoft.com/dotnet/fsharp
open System
open Microsoft.FSharpLu
open Newtonsoft
type RESTlerBugFilePosition =
| Intro
| Request
type RestlerRequestDetails =
{
producerTimingDelay : int option
maxAsyncWaitTime : int option
previousResponse : string option
}
static member Empty =
{
producerTimingDelay = None
maxAsyncWaitTime = None
previousResponse = None
}
type RestlerRequest =
{
httpMethod : string
query :string
headers : Map<string, string>
body: string option
restlerRequestDetails : RestlerRequestDetails
}
type RESTlerBug =
{
bugName : string option
bugHash: string option
requests : RestlerRequest list
}
static member Empty =
{
bugName = None
bugHash = None
requests = []
}
module Constants =
let [<Literal>] IntroSeparator = "################################################################################"
let [<Literal>] Hash = "Hash:"
let [<Literal>] Request = "->"
let [<Literal>] PreviousResponse = "PREVIOUS RESPONSE:"
let [<Literal>] Host = "Host:"
let [<Literal>] Accept = "Accept:"
let [<Literal>] ContentType = "Content-Type:"
module RequestConfig =
let [<Literal>] ProducerTimerDelay = "! producer_timing_delay"
let [<Literal>] MaxAsyncWaitTime = "! max_async_wait_time"
let parseRequest (x: string) =
let r = x.Substring(Constants.Request.Length).Split("\\r\\n") |> List.ofArray
let method, query =
match ((List.head r).Trim().Split(' ')) |> List.ofArray with
| m :: q :: _ -> m.Trim(), q.Trim()
| _ -> failwithf "Unhandled RESTler log format (expected HTTP method followed by query): %s" (r.[0])
//from r.[1] to end, but if "" line and not last one -> then next one is body
let rec collectHeaders (headers: Map<string, string>) (rs: string list) =
let parseHeader (h: string) =
match h.Split(':') with
| [|k; v|] -> Some(k.Trim(), v.Trim())
| _ -> None
match rs with
| "" :: body :: "" :: [] -> headers, Some body
| r :: "" :: [] ->
match parseHeader r with
| Some(k, v) ->
(Map.add k v headers), None
| None -> headers, None
| r :: rs ->
match parseHeader r with
| Some(k, v) -> collectHeaders (Map.add k v headers) rs
| None -> collectHeaders headers rs
| [] -> headers, None
let headers, body = collectHeaders Map.empty (List.tail r)
let body =
match body with
| Some s ->
let b = Json.Compact.deserialize(s.Replace("\\n", ""))
Some(b.ToString())
| None -> None
{|
HttpMethod = method
Query = query
Headers= headers
Body = body
|}
let parseRESTlerBugFound(bugFound: string list) =
let rec parse (xs: string list) (bugDefinition : RESTlerBug) (pos: RESTlerBugFilePosition) =
match pos, xs with
| Intro, (Constants.IntroSeparator::[]) -> None
| Intro, (Constants.IntroSeparator::y::xs) when not (String.IsNullOrWhiteSpace y) ->
parse xs {bugDefinition with bugName = Some y} Intro
| Intro, x::xs when x.Trim().StartsWith(Constants.Hash) ->
parse xs {bugDefinition with bugHash = Some (x.Trim().Substring(Constants.Hash.Length)) } Intro
| Intro, (Constants.IntroSeparator::y::xs) when String.IsNullOrWhiteSpace y ->
parse xs bugDefinition Request
// Skip rest of bug definition intro
| Intro, x::xs ->
if not (String.IsNullOrWhiteSpace x) then
printfn "Ignoring: %s" x
parse xs bugDefinition Intro
| Request, (x :: xs) when String.IsNullOrEmpty x -> parse xs bugDefinition Request
| Request, (x :: xs) when x.StartsWith Constants.Request ->
let r = parseRequest x
let rec restlerRequestDetails (xs: string list) (requestDetails: RestlerRequestDetails) =
match xs with
| y :: ys when y.StartsWith(Constants.RequestConfig.MaxAsyncWaitTime) ->
let t = y.Substring(Constants.RequestConfig.MaxAsyncWaitTime.Length)
match Int32.TryParse t with
| true, n -> restlerRequestDetails ys {requestDetails with maxAsyncWaitTime = Some n}
| false, _ -> failwithf "Expected max async wait time to be an integer: %s" y
| y :: ys when y.StartsWith(Constants.RequestConfig.ProducerTimerDelay) ->
let t = y.Substring(Constants.RequestConfig.ProducerTimerDelay.Length)
match Int32.TryParse t with
| true, n -> restlerRequestDetails ys {requestDetails with producerTimingDelay = Some n}
| false, _ -> failwithf "Expected producer timing delay to be an integer: %s" y
| y :: ys when y.StartsWith(Constants.PreviousResponse) ->
{requestDetails with previousResponse = Some (y.Substring(Constants.PreviousResponse.Length))}, ys
| s :: _ -> failwithf "Unhandled RESTler request details : %s" s
| ss -> failwithf "Unhandled case when processing RESTler request details : %A" ss
let requestDetails, rest = restlerRequestDetails xs RestlerRequestDetails.Empty
let request: RestlerRequest =
{
httpMethod = r.HttpMethod
query = r.Query
headers = r.Headers
body = r.Body
restlerRequestDetails = requestDetails
}
parse rest { bugDefinition with requests = bugDefinition.requests @ [request] } Request
| _ -> Some bugDefinition
parse (bugFound |> Seq.toList) RESTlerBug.Empty Intro
module Postman =
type Name = Json.JsonPropertyAttribute
type Info =
{
[<Name("_postman_id")>]
PostmanId: System.Guid
[<Name("name")>]
Name: string
[<Name("schema")>]
Schema: string
}
static member Create(name: string) =
{
PostmanId = System.Guid.NewGuid()
Name = name
Schema = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
}
type Query =
{
[<Name("key")>]
Key : string
[<Name("value")>]
Value : string
}
type Url =
{
[<Name("raw")>]
Raw : string
[<Name("protocol")>]
Protocol: string
[<Name("host")>]
Host : string array
[<Name("path")>]
Path : string array
[<Name("query")>]
Query : Collections.Specialized.NameValueCollection option
}
type BodyLanguage =
{
[<Name("language")>]
Language : string
}
type BodyOptions =
{
[<Name("raw")>]
Raw : BodyLanguage
}
type Body =
{
[<Name("mode")>]
Mode: string
[<Name("raw")>]
Raw: string
[<Name("options")>]
Options : BodyOptions
}
type Header =
{
[<Name("key")>]
Key : string
[<Name("value")>]
Value : string
}
type Request =
{
[<Name("method")>]
Method : string
[<Name("header")>]
Header : Header array
[<Name("body")>]
Body : Body option
[<Name("url")>]
Url : Url
}
type Response =
{
[<Name("code")>]
Code : int
}
type SystemHeaders =
{
[<Name("accept")>]
Accept : bool
[<Name("content-type")>]
ContentType : bool
}
type ProtocolProfileBehaviour =
{
[<Name("disabledSystemHeaders")>]
DisabledSystemHeaders: SystemHeaders
}
type Item =
{
[<Name("name")>]
Name : string
[<Name("protocolProfileBehavior")>]
ProtocolProfileBehavior : ProtocolProfileBehaviour
[<Name("request")>]
Request : Request
[<Name("response")>]
Response : Response array
}
type Collection =
{
[<Name("info")>]
Info : Info
[<Name("item")>]
Item : Item list
}
let convertToPostmanFormat (useSsl: bool) (name: string) (bug : RESTlerBug) =
let info = Info.Create(name)
let requests =
bug.requests
|> List.map(fun r ->
let host = r.headers.["Host"]
let headers = r.headers.Remove("Host")
{
Name = sprintf "%s:%s" r.httpMethod (r.query.Substring(0, (min 24 r.query.Length)))
ProtocolProfileBehavior = {DisabledSystemHeaders = {Accept = true; ContentType = true}}
Request =
{
Method = r.httpMethod
Header =
headers
|> Map.toArray
|> Array.map (fun (k, v) -> {Key = k; Value = v})
Body =
r.body |> Option.map(fun b ->
{
Mode = "raw"
Raw = b
Options = {Raw = {Language = "json"}}
}
)
Url =
let protocol = if useSsl then "https" else "http"
let uri = System.Uri(sprintf "%s://%s%s" protocol host r.query)
let parsedQuery = System.Web.HttpUtility.ParseQueryString(uri.Query)
{
Raw = uri.AbsoluteUri
Protocol = protocol
Host = host.Split('.')
Path = uri.AbsolutePath.Split('/') |> Array.filter(fun s -> not <| String.IsNullOrEmpty s)
Query =
if parsedQuery.Count = 0 then
None
else
Some parsedQuery
}
}
Response = [||]
}
)
{
Info = info
Item = requests
}
module CommandLine =
let [<Literal>] UseSSL = "--use-ssl"
let [<Literal>] RESTlerBugBucket = "--restler-bug-bucket-path"
let parse (args: string array) =
let rec parse (r : {| UseSsl : bool; RESTlerBugBucketPath : string option |}) (xs : string list) =
match xs with
| [] -> r
| UseSSL :: xs ->
parse {| r with UseSsl = true |} xs
| RESTlerBugBucket :: p :: xs ->
parse {| r with RESTlerBugBucketPath = Some p |} xs
| s -> failwithf "Unhandled command line parameters: %A" s
let r = parse {| UseSsl = false; RESTlerBugBucketPath = None |} (List.ofArray args)
match r.RESTlerBugBucketPath with
| None ->
failwithf "Expected RESTler bug bucket as an input. Parameters: %s %s [path]" UseSSL RESTlerBugBucket
| Some p -> {| UseSSL = r.UseSsl; RESTlerBugBucketPath = p |}
[<EntryPoint>]
let main argv =
let config = CommandLine.parse argv
let fileContents =
use stream = System.IO.File.OpenText(config.RESTlerBugBucketPath)
[
while not stream.EndOfStream do
yield stream.ReadLine().Trim()
]
match parseRESTlerBugFound fileContents with
| None ->
printfn "Failed to parse RESTler bug information from : %s" config.RESTlerBugBucketPath
1
| Some bug ->
let bugFileName = System.IO.Path.GetFileNameWithoutExtension(config.RESTlerBugBucketPath)
let p = System.IO.Path.ChangeExtension(config.RESTlerBugBucketPath, ".postman.json")
let postmanCollection = Postman.convertToPostmanFormat config.UseSSL (sprintf "%s [%s]" bugFileName bug.bugHash.Value) bug
printfn "Writing Postman collection to: %s" p
Json.Compact.serializeToFile p postmanCollection
0 // return an integer exit code

Просмотреть файл

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.FSharpLu.Json" Version="0.11.6" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="5.0.1" />
</ItemGroup>
</Project>

Просмотреть файл

@ -7,7 +7,9 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "RestlerAgent", "RESTlerAgen
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "RaftResultAnalyzer", "RaftResultAnalyzer\RaftResultAnalyzer.fsproj", "{32475F74-7C03-4547-ACF2-C02B1BF6F85A}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "AzureAuth", "AzureAuth\AzureAuth.fsproj", "{8A202DC0-DBEC-425C-8504-BCF3FDD1AE4B}"
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "AzureAuth", "AzureAuth\AzureAuth.fsproj", "{8A202DC0-DBEC-425C-8504-BCF3FDD1AE4B}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "RESTler2Postman", "RESTler2Postman\RESTler2Postman.fsproj", "{9483A0A4-0FD0-4905-BDA9-72A8FE70D670}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -27,6 +29,10 @@ Global
{8A202DC0-DBEC-425C-8504-BCF3FDD1AE4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A202DC0-DBEC-425C-8504-BCF3FDD1AE4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A202DC0-DBEC-425C-8504-BCF3FDD1AE4B}.Release|Any CPU.Build.0 = Release|Any CPU
{9483A0A4-0FD0-4905-BDA9-72A8FE70D670}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9483A0A4-0FD0-4905-BDA9-72A8FE70D670}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9483A0A4-0FD0-4905-BDA9-72A8FE70D670}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9483A0A4-0FD0-4905-BDA9-72A8FE70D670}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

Просмотреть файл

@ -28,6 +28,8 @@ module private RESTlerInternal =
let resultAnalyzer =
"/raft" ++ "result-analyzer" ++ "RaftResultAnalyzer.dll"
let postmanConverter =
"/raft" ++ "restler2postman" ++ "RESTler2Postman.dll"
(*
let SupportedCheckers =
@ -85,24 +87,29 @@ module private RESTlerInternal =
// buffer and waits for the parent to consume it, and the parent waits for the
// child process to exit first.
// Reference: https://stackoverflow.com/questions/139593/processstartinfo-hanging-on-waitforexit-why?lq=1
use stdOutFile =
match stdOutFilePath with
| Some path -> System.IO.File.CreateText(path) :> IO.TextWriter
| None -> IO.TextWriter.Null
use stdErrFile =
match stdErrFilePath with
| Some path -> System.IO.File.CreateText(path) :> IO.TextWriter
| None -> IO.TextWriter.Null
let stdOutFile =
lazy(
match stdOutFilePath with
| Some path -> System.IO.File.CreateText(path) :> IO.TextWriter
| None -> IO.TextWriter.Null
)
let stdErrFile =
lazy(
match stdErrFilePath with
| Some path -> System.IO.File.CreateText(path) :> IO.TextWriter
| None -> IO.TextWriter.Null
)
let appendHandler
(endOfStreamEvent:System.Threading.AutoResetEvent)
(aggregator:IO.TextWriter)
(aggregator:Lazy<IO.TextWriter>)
(dataReceived:Diagnostics.DataReceivedEventArgs) =
if isNull dataReceived.Data then
if not endOfStreamEvent.SafeWaitHandle.IsClosed && not endOfStreamEvent.SafeWaitHandle.IsInvalid then
endOfStreamEvent.Set() |> ignore
else
aggregator.WriteLine(dataReceived.Data) |> ignore
aggregator.Value.WriteLine(dataReceived.Data) |> ignore
instance.OutputDataReceived.Add(appendHandler noMoreOutput stdOutFile)
instance.ErrorDataReceived.Add(appendHandler noMoreError stdErrFile)
@ -123,17 +130,22 @@ module private RESTlerInternal =
None
try
do! stdOutFile.FlushAsync() |> Async.AwaitTask
if stdOutFile.IsValueCreated then
do! stdOutFile.Value.FlushAsync() |> Async.AwaitTask
with ex ->
printfn "Failed to flush stdoout due to %A" ex
try
do! stdErrFile.FlushAsync() |> Async.AwaitTask
if stdErrFile.IsValueCreated then
do! stdErrFile.Value.FlushAsync() |> Async.AwaitTask
with ex ->
printfn "Failed to flush stderr due to %A" ex
stdOutFile.Close()
stdErrFile.Close()
if stdOutFile.IsValueCreated then
stdOutFile.Value.Close()
if stdErrFile.IsValueCreated then
stdErrFile.Value.Close()
return
{
@ -317,6 +329,19 @@ module private RESTlerInternal =
}
let convertBugBucketToPostmanCollection (useSsl: bool) (bugFilePath: string) =
async {
let! result =
startProcessAsync
Runtime.DotNet
(sprintf "\"%s\" %s %s" Paths.postmanConverter (if useSsl then "--use-ssl" else "") (sprintf "--restler-bug-bucket-path \"%s\"" bugFilePath))
"."
None
(Some (sprintf "%s.convert.err.txt" bugFilePath))
return ()
}
let test testType restlerRootDirectory workingDirectory (parameters: Raft.RESTlerTypes.Engine.EngineParameters) =
async {
do!
@ -461,7 +486,9 @@ let getListOfBugs workingDirectory (runStartTime: DateTime) =
let bugFoundPollInterval = TimeSpan.FromSeconds (10.0)
type OnBugFound = Map<string, string> -> Async<unit>
let pollForBugFound workingDirectory (token: Threading.CancellationToken) (runStartTime: DateTime) (ignoreBugHashes: string Set) (onBugFound : OnBugFound) =
type ConvertBugBucket = string -> Async<unit>
let pollForBugFound workingDirectory (token: Threading.CancellationToken) (runStartTime: DateTime) (ignoreBugHashes: string Set) (onBugFound : OnBugFound) (convert: ConvertBugBucket) =
let rec poll() =
async {
if token.IsCancellationRequested then
@ -493,6 +520,7 @@ let pollForBugFound workingDirectory (token: Threading.CancellationToken) (runSt
|> Seq.map (fun (KeyValue(bugHash, bugFile)) ->
async {
if not <| postedBugs.Contains bugHash then
do! convert (experiment.FullName ++ "bug_buckets" ++ bugFile.file_path)
do! onBugFound (Map.empty.Add("Experiment", experiment.Name).Add("BugBucket", bugFile.file_path).Add("BugHash", bugHash))
return bugHash
}
@ -524,7 +552,7 @@ let test (testType: string)
token.Cancel()
}
resultAnalyzer workingDirectory token.Token report (runStartTime, reportInterval)
pollForBugFound workingDirectory token.Token runStartTime ignoreBugHashes onBugFound
pollForBugFound workingDirectory token.Token runStartTime ignoreBugHashes onBugFound (RESTlerInternal.convertBugBucketToPostmanCollection parameters.UseSsl)
]
return ()
}
@ -545,7 +573,7 @@ let fuzz (fuzzType: string)
token.Cancel()
}
resultAnalyzer workingDirectory token.Token report (runStartTime, reportInterval)
pollForBugFound workingDirectory token.Token runStartTime ignoreBugHashes onBugFound
pollForBugFound workingDirectory token.Token runStartTime ignoreBugHashes onBugFound (RESTlerInternal.convertBugBucketToPostmanCollection parameters.UseSsl)
]
return ()
}

Просмотреть файл

@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orchestrator", "Orchestrato
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "OrchestratorLogic", "Orchestrator\OrchestratorLogic\OrchestratorLogic.fsproj", "{BFE2B877-4015-4519-92D8-F6A633770B15}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "RESTler2Postman", "Agent\RESTler2Postman\RESTler2Postman.fsproj", "{3B83CE52-07BB-4CA6-9AA6-B0962E064330}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -63,6 +65,10 @@ Global
{BFE2B877-4015-4519-92D8-F6A633770B15}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFE2B877-4015-4519-92D8-F6A633770B15}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFE2B877-4015-4519-92D8-F6A633770B15}.Release|Any CPU.Build.0 = Release|Any CPU
{3B83CE52-07BB-4CA6-9AA6-B0962E064330}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B83CE52-07BB-4CA6-9AA6-B0962E064330}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B83CE52-07BB-4CA6-9AA6-B0962E064330}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B83CE52-07BB-4CA6-9AA6-B0962E064330}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE