Merged PR 3090: fix some typos.

This commit is contained in:
Chris Lovett 2020-09-30 01:42:00 +00:00
Родитель 182af6e1c9
Коммит 68702c2652
1 изменённых файлов: 72 добавлений и 60 удалений

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

@ -11,9 +11,8 @@ permalink: /learn/tutorials/image-gallery-aspnet
This sample shows how to use Coyote to **systematically test** an ASP.NET service. The sample
ASP.NET service included here implements a simple **image gallery**. Please note that this sample
service is *not* fully-fledged and contains some interesting *bugs* on purpose. You can run the unit
tests with and without Coyote, but you cannot actually deploy the sample (as some production logic
is missing).
service contains some interesting *bugs* on purpose. You can run the unit tests with and without
Coyote and you can run the web front end using the Azure storage emulators.
<img class="img-responsive" src="/coyote/assets/images/ImageGallery.png" alt="screenshot">
@ -38,6 +37,16 @@ must be installed and running.
- Open Azure Cosmos Data Explorer from taskbar tray and copy the `Primary Connection String` from
there into `~/ImageGalleryAspNet/ImageGalleryService/appsettings.json`.
## Build the sample
After cloning the [Coyote Samples](http://github.com/microsoft/coyote-samples) git repo run the
following command:
```
cd ImageGalleryAspNet
dotnet build
```
## Running the sample
You do not need to do this unless you really want to, you can do all the unit testing using coyote
@ -48,21 +57,21 @@ Once all the above prerequisites are complete (including the storage emulators)
app using two separate console windows as follows:
```shell
cd ..\ImageGalleryService
cd ImageGalleryService
dotnet run
```
And in the second window:
```shell
cd ..\ImageGallery
cd ImageGallery
dotnet run
```
Navigate your web browser to `http://localhost:5000/` and you will see the empty ImageGallery site.
Upload some of your favorite photos (you can do more than one at a time) and you will see them
appear. The "dotnet run" consoles will also show logging output that shows you what is happening
behind the scenes.
Create a new test account and upload some of your favorite photos (you can do more than one at a
time) and you will see them appear. The "dotnet run" consoles will also show logging output that
shows you what is happening behind the scenes.
## Sample structure
@ -81,7 +90,7 @@ This service is based on a 3-tier architecture: (1) a client (implemented by the
the ImageGallery web front end), (2) two ASP.NET API services, one for managing accounts
(`AccountController`) and one for managing image galleries (`GalleryController`) associated with
these accounts, and (3) two backend storage systems used by these API services (Cosmos DB to
stores accounts, and Azure Blob Storage to stores images).
store accounts, and Azure Blob Storage to store images).
The `AccountController` has 4 APIs to `Create`, `Update`, `Get` and `Delete` an account. `Create`
first checks if the account already exists, and if not it creates it in Cosmos DB. Similarly,
@ -90,21 +99,21 @@ accordingly. Finally, `Delete` first checks if the account exists, and if it doe
Cosmos DB and then also deletes the associated image container for this account from Azure Blob
Storage.
The `GalleryController` is similar. It has 3 APIs to `Store`, `Get` and `Delete` an image. `Store`
The `GalleryController` is similar. It has 4 APIs: `Store`, `Get`, `Delete` `GetList`. `Store`
first checks if the account already exists in Cosmos DB and, if it does, it stores the image in
Azure Blob Storage. `Get` is simple, it checks if the image blob exists, if it it does, it returns
the image. Finally, `Delete` first checks if the account already exists in Cosmos DB and, if it
does, it deletes the image blob from Azure Blob Storage. There is also a `GetList` API to enumerate
the images in an account.
does, it deletes the image blob from Azure Blob Storage. `GetList` can be used to enumerate
the images in an account in pages.
## Bugs in the service
As mentioned above, this service was designed in purpose to be contain some interesting bugs to
As mentioned above, this service was designed in purpose to contain some interesting bugs to
showcase systematic testing with Coyote. You can read the comments in the controllers for more
details about the bugs, but briefly a lot of the above APIs have race conditions. For example, the
controller checks that the account exists, and tries to update it, but another request deletes the
account after the exists check but before the update, resulting in an unhandled exception, and thus
a 500 internal server error, which is a bad bug. The service should never return a 500 to its users!
controller checks that the account exists, and tries to update it, but another request could delete
the account between those two operations, resulting in an unhandled exception, and thus a 500
internal server error, which is a bad bug. The service should never return a 500 to its users!
These kind of bugs involving race conditions between concurrent requests are hard to test. Unit
tests are typically tailored to test sequential programs. Async race conditions can result in flaky
@ -115,26 +124,27 @@ We wrote two unit tests (using `MSTest` and `Microsoft.AspNetCore.Mvc.Testing`)
service and uncover bugs: `TestConcurrentAccountRequests` and
`TestConcurrentAccountAndImageRequests`. The former only tests the `AccountController`, while the
later tests both controllers at the same time. You can read more details in the comments for these
tests, but let us quickly summarize what these tests do and what the bugs are:
tests, but let us quickly summarize what these tests do and what the bugs are.
The `TestConcurrentAccountRequests` method initializes the mocks and injects them into the ASP.NET
service, then creates a client, which does a `Create` account request to create a new account for
Alice. It waits for this request to complete. It then *concurrently* invokes two requests: `Update`
account and `Delete` account, and waits for both to complete. These requests are now racing, and
because there is a race condition in the `AccountController` controller logic, the update request
can nondeterministically (not every time you run the test!) fail due to an unhandled exception (500
error code). The issue is that the controller first checks if the account exists and, if it does, it
then updates it. But after the "does account exists check", the delete request could run, deleting
the account! The update then tries to happen and BAM there is a bug! Interestingly the non-coyote
test run rarely finds this bug.
can fail due to an unhandled exception (500 error code) and this failure is nondeterministic. The
issue is that the controller first checks if the account exists and, if it does, it then updates
it. But after the "does account exists check", the delete request could run, deleting the account!
The update then tries to run which triggers the bug! Interestingly the non-coyote test run rarely
finds this bug although it is possible to see it with teh ImageGallery web front end if you use
multiple browser windows and do batch upload and delete operations in each.
The `TestConcurrentAccountAndImageRequests` initializes the mocks and injects them into the ASP.NET
service, then creates a client, which does a `Create` account request to create a new account for
Alice. It waits for this request to complete. It then *concurrently* invokes two requests: `Store`
image and `Delete` account, and waits for both to complete. Similar to the above test, these
requests are now racing, and because there is a race condition in the controller logic, the store
request can nondeterministically store the image in an "orphan" container in Azure Storage, even if
the associated account was deleted. This is more subtle bug because the request itself is not
request can nondeterministically store the image in an "orphan" container in Azure Storage, even
if the associated account was deleted. This is more subtle bug because the request itself is not
throwing an unhandled exception, it is just a sneaky race when trying to read/write from/to two
backend systems at the same time. This issue is also a data race and you can see the detail in the
controller logic.
@ -148,14 +158,6 @@ the request failed.
Before going further (and into Coyote), lets build and run the tests!
## Build the samples
Build the `coyote-samples` repo by running the following command:
```
powershell -f build.ps1
```
## How to run the unit tests
Just run them from inside Visual Studio, or run the following:
@ -168,9 +170,9 @@ dotnet test bin/netcoreapp3.1/ImageGalleryTests.dll
The tests may or may not trigger the bug! Most likely you will see this output:
```
Test Run Successful.
Total tests: 3
Passed: 3
Total tests: 2
Passed: 2
Total time: 1.2053 Seconds
```
And even if you do get them to fail, if you try to debug them, the bugs may or may not
@ -183,19 +185,16 @@ You can learn more about the systematic testing capabilities of Coyote
to get quickly in action.
Coyote serializes the execution of a concurrent program (i.e., only executes a single task at at
time). A Coyote test executes this serialized program lots of times (we call these testing
time). A Coyote test executes this serialized program lots of times (called testing
iterations), each time exploring different interleavings in a clever manner. If a bug is found,
Coyote gives a trace that allows you to deterministically reproduce the bug. You can use the VS
Debugger to go over the trace as many times as required to fix the bug.
To be able to test this service, we use Coyote's binary rewriting capabilities to instrument
To be able to test this service, use Coyote's binary rewriting capabilities to instrument
concurrency primitives (Coyote primarily supports common task-related types like `Task` and
`TaskCompletionSource`, the `lock` statement, and some
[more](https://microsoft.github.io/coyote/learn/programming-models/async/rewriting)). This is
handled by our build script above. If you end up reading our website, or see some of the production
code that uses Coyote today, you will notice they are using a custom task type
`Microsoft.Coyote.Tasks.Task`, this is being replaced by the binary rewriting described here (and in
our website), as it makes things so much easier to use Coyote.
handled by the build script above.
## How to run the Coyote systematic tests
@ -205,13 +204,9 @@ the tests, run the following from the root directory of the repo:
```
coyote rewrite rewrite.coyote.json
cd "bin/coyote"
dotnet test ImageGalleryTests.Coyote.dll
cd "../.."
dotnet test bin/coyote/ImageGalleryTests.Coyote.dll
```
**Note:** this is `./ImageGalleryAspNet/bin/coyote` and not `./ImageGalleryAspNet/bin/netcoreapp3.1`.
This will [rewrite](/coyote/learn/tools/rewriting) the tests, and then run the tests inside the
Coyote testing engine, up to 1000 iterations each, and report any found bugs. The bug should be
found most of the time after just a few iterations (as they are not too deep).
@ -232,13 +227,11 @@ Which also tells you how to reliably reproduce the bug using Coyote.
As you can see above, the `TestConcurrentAccountRequests` failed. This bug is nondeterministic, and
if you try debug it without Coyote it might not always happen. However, Coyote gives you a reliable
repro. Right now, someone can use the replay functionality from the `coyote replay` tool or
programmatically through our replay API, but for the purposes of this sample, we put together a
simple `TraceReplayer` executable that takes the name of the test and the trace file produced by
Coyote, and replays it in the VS debugger. To do this, just invoke the command mentioned in the
error above (change the paths to the ones on your machine):
```
TraceReplayer.exe TestConcurrentAccountRequests TestConcurrentAccountRequests.schedule
```
programmatically through the replay API, but for the purposes of this sample there is a simple
`TraceReplayer` executable that takes the name of the test and the trace file produced by Coyote,
and replays it in the VS debugger. To do this, just invoke the command mentioned in the error above
(change the paths to the ones on your machine): ``` TraceReplayer.exe TestConcurrentAccountRequests
TestConcurrentAccountRequests.schedule ```
You will also see that the trace output contains logs such as:
```
@ -260,9 +253,9 @@ You will also see that the trace output contains logs such as:
In the above logs, the `[0HM34OD7O65E5]` prefix is the `HttpContext.TraceIdentifier` which is a
unique id per HTTP request. This makes it easier to see how Coyote explores lots of async
interleaving during testing and debugging. In this example we see it bouncing between 3 async tasks
`[0HM34OD7O65E5]`, `[0HM34OD7O65E6]` and `[0HM34OD7O65E7]` and this controlled interleaving of
tasks can help find lots of bugs.
interleaving during testing and debugging. In this example you can see it bouncing between 3 async
tasks `[0HM34OD7O65E5]`, `[0HM34OD7O65E6]` and `[0HM34OD7O65E7]` and this controlled interleaving
of tasks can help find lots of bugs.
## Rewriting unit tests
@ -272,14 +265,32 @@ following:
```
coyote rewrite rewrite.coyote.json --rewrite-unit-tests --iterations 100
cd "bin/coyote"
dotnet test ImageGalleryTests.dll
cd "../.."
dotnet test bin/coyote/ImageGalleryTests.dll
```
This changes the non-Coyote unit tests so that they first create a Coyote `TestEngine` then run the
original unit test.
original unit test. You will find that this also finds the same bugs, you should see output like this:
```
X TestConcurrentAccountAndImageRequestsAsync [252ms]
Error Message:
The image was not deleted from Azure Blob Storage.
```
## Troubleshooting
**Login fails with an unhandled exception has occurred while executing the request.**
Make sure the Azure storage emulators are running.
**SocketException: No connection could be made because the target machine actively refused it.**
Make sure the Azure storage emulators are running.
**System.AggregateException: 'Retry failed after 6 tries**
Make sure the Azure storage emulators are running. Make sure you can then connect to the azure
storage emulator from the Azure storage `explorer`.
## Summary
@ -292,3 +303,4 @@ In this tutorial you learned:
4. How to replay a buggy trace of unmodified code with Coyote.
Happy debugging!