Repository: microsoft/debugpy

Issue:
Regression compared to ptvsd: debugging python running in a bazel runtime sandbox causes duplicate tabs to open
## Context

When using python in a bazel sandbox (`bazel run //python/target`), bazel symlinks the required files into a 'runfiles' sandbox. My approach to debugging is to run it under the debugger and make it wait for client to connect (formerly ptvsd, now I'm exploring switching to debugpy). 

Once attached, breakpoints trigger, but open a copy of the file at the sandbox path (with some visual quirks, such as that breakpoints don't visually sync between both copies, see screenshot). This pollutes tabs and makes debugging messy, especially when doing complex debugging sessions that involve many files.

![image](https://user-images.githubusercontent.com/6514289/136251480-00f639a2-be26-40a3-88e5-a2209e19c8c2.png)
This screenshot shows both the double tabs issue, as well as they breakpoints visually not synchronizing between both copies of the same file.

With `ptvsd`, there was a simple workaround for this issue, just add this to the launch.json debug config:

```json
"pathMappings": [
    {"localRoot": "${workspaceFolder}", "remoteRoot": "${workspaceFolder}"}
],
```

This seemingly no-op mapping effectively solved this issue, presumably because as part of the path mapping, it canonicalizes the path (like `os.path.realpath` in python, or `readlink -f` in bash for macos and linux) in the process, causing breakpoints to cleanly trigger in the original files.

You might as yourself: why not just do this:

```json
"pathMappings": [
    {"localRoot": "${workspaceFolder}", "remoteRoot": "<sandbox_path>"}
],
```

This isn't tractable for 2 reasons:
1. The bazel sandbox path has a random hash in it, so updating the path mapping accordingly for each workspace would be annoying
2. The bazel sandbox symlinks individual files and the sandbox doesn't share the same folder structure as the workspace, i.e. files might be in some folder structure in the workspace, but are symlinked into a different folder structure in the sandbox. This means we'd need many pathmappings to get this to work and those will vary from bazel target to target. This is not manually doable in practice (unless solved programmatically perhaps).

I'd like to switch from `ptvsd` to `debugpy`, as debugpy has significantly better multiprocess handling. But this issue is a major blocker. 

## Environment data

- debugpy version: 1.5.0
- OS and version: Ubuntu 18.04 (accessed via remote ssh plugin from MacOS)
- Python version (& distribution if applicable, e.g. Anaconda): 3.9 (from apt: python3.9)
- Using VS Code or Visual Studio: VS Code

## Actual behavior

* With `ptvsd` the pathmapping trick works to avoid opening sandbox copies of files.
* With `dedubpy` the pathmapping trick does not work to avoid opening sandbox copies of files.

## Expected  behavior

I'd expect both to work. Either through the pathmappings trick, or through some other way such as introducing a new that `canonicalizes` paths.

As I believe `debugpy` was forked from `ptvsd`, we could maybe compare code and see what tweak is needed to make the pathmappings trick work again. 

## Steps to reproduce:

I've created a small example environment in which this issue can easily be reproduced. Please open this folder as a workspace in vscode and follow the instructions in the readme. 

[debugpy_issue.zip](https://github.com/microsoft/debugpy/files/7296047/debugpy_issue.zip)



Discussion:
microsoft-cla[bot]: [![CLA assistant check](https://cla.opensource.microsoft.com/pull/badge/not_signed)](https://cla.opensource.microsoft.com/microsoft/debugpy?pullRequest=923) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla.opensource.microsoft.com/microsoft/debugpy?pullRequest=923) before we can accept your contribution.<br/><br/>:x: fabioz [sign now](https://cla.opensource.microsoft.com/microsoft/debugpy?pullRequest=923)<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla.opensource.microsoft.com/check/microsoft/debugpy?pullRequest=923) it.</sub>
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=microsoft_debugpy&pullRequest=923)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=923&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=923&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=923&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=923&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=923&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=microsoft_debugpy&pullRequest=923)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=923&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=923&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=923&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=923&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=923&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=microsoft_debugpy&pullRequest=923)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=923&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=923&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=923&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=923&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=923&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=923&metric=duplicated_lines_density&view=list) No Duplication information



Patch:
diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
index 7922f45c..c1ae27cf 100644
--- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
+++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
@@ -388,6 +388,10 @@ class PyDevJsonCommandProcessor(object):
         if bool(path_mappings):
             pydevd_file_utils.setup_client_server_paths(path_mappings)
 
+        resolve_symlinks = args.get('resolveSymlinks', None)
+        if resolve_symlinks is not None:
+            pydevd_file_utils.set_resolve_symlinks(resolve_symlinks)
+
         redirecting = args.get("isOutputRedirected")
         if self._options.redirect_output:
             py_db.enable_output_redirection(True, True)
diff --git a/src/debugpy/_vendored/pydevd/pydevd_file_utils.py b/src/debugpy/_vendored/pydevd/pydevd_file_utils.py
index 940d32ee..ffe0d7f6 100644
--- a/src/debugpy/_vendored/pydevd/pydevd_file_utils.py
+++ b/src/debugpy/_vendored/pydevd/pydevd_file_utils.py
@@ -43,7 +43,7 @@ r'''
 
 from _pydev_bundle import pydev_log
 from _pydevd_bundle.pydevd_constants import DebugInfoHolder, IS_WINDOWS, IS_JYTHON, \
-    DISABLE_FILE_VALIDATION
+    DISABLE_FILE_VALIDATION, is_true_in_env
 from _pydev_bundle._pydev_filesystem_encoding import getfilesystemencoding
 from _pydevd_bundle.pydevd_comm_constants import file_system_encoding, filesystem_encoding_is_utf8
 from _pydev_bundle.pydev_log import error_once
@@ -401,6 +401,9 @@ def _abs_and_canonical_path(filename, NORM_PATHS_CONTAINER=NORM_PATHS_CONTAINER)
 
         isabs = os_path_isabs(filename)
 
+        if _global_resolve_symlinks:
+            os_path_abspath = os_path_real_path
+
         normalize = False
         abs_path = _apply_func_and_normalize_case(filename, os_path_abspath, isabs, normalize)
 
@@ -643,6 +646,14 @@ def get_frame_id_from_source_reference(source_reference):
     return _source_reference_to_frame_id.get(source_reference)
 
 
+_global_resolve_symlinks = is_true_in_env('PYDEVD_RESOLVE_SYMLINKS')
+
+
+def set_resolve_symlinks(resolve_symlinks):
+    global _global_resolve_symlinks
+    _global_resolve_symlinks = resolve_symlinks
+
+
 def setup_client_server_paths(paths):
     '''paths is the same format as PATHS_FROM_ECLIPSE_TO_PYTHON'''
 
diff --git a/src/debugpy/common/json.py b/src/debugpy/common/json.py
index d5bd12da..6f3e2b21 100644
--- a/src/debugpy/common/json.py
+++ b/src/debugpy/common/json.py
@@ -7,6 +7,7 @@
 
 import builtins
 import json
+import numbers
 import operator
 
 
@@ -92,6 +93,16 @@ class JsonObject(object):
 # some substitutions - e.g. replacing () with some default value.
 
 
+def _converter(value, classinfo):
+    """Convert value (str) to number, otherwise return None if is not possible"""
+    for one_info in classinfo:
+        if issubclass(one_info, numbers.Number):
+            try:
+                return one_info(value)
+            except ValueError:
+                pass
+
+
 def of_type(*classinfo, **kwargs):
     """Returns a validator for a JSON property that requires it to have a value of
     the specified type. If optional=True, () is also allowed.
@@ -107,6 +118,10 @@ def of_type(*classinfo, **kwargs):
         if (optional and value == ()) or isinstance(value, classinfo):
             return value
         else:
+            converted_value = _converter(value, classinfo)
+            if converted_value:
+                return converted_value
+
             if not optional and value == ():
                 raise ValueError("must be specified")
             raise TypeError("must be " + " or ".join(t.__name__ for t in classinfo))


Test patch:
diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
index 69f74c53..d29c8ea1 100644
--- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
+++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
@@ -6088,6 +6088,43 @@ def test_replace_process(case_setup_multiprocessing):
         writer.finished_ok = True
 
 
+@pytest.mark.parametrize('resolve_symlinks', [True, False])
+def test_use_real_path_and_not_links(case_setup, tmpdir, resolve_symlinks):
+    dira = tmpdir.join('dira')
+    dira.mkdir()
+
+    dirb = tmpdir.join('dirb')
+    dirb.mkdir()
+
+    original_file = dira.join('test.py')
+    original_file.write('''
+print('p1')  # Break here
+print('p2')
+print('TEST SUCEEDED')
+''')
+
+    symlinked_file = dirb.join('testit.py')
+    os.symlink(str(original_file), str(symlinked_file))
+
+    # I.e.: we're launching the symlinked file but we're actually
+    # working with the original file afterwards.
+    with case_setup.test_file(str(symlinked_file)) as writer:
+        json_facade = JsonFacade(writer)
+
+        writer.write_add_breakpoint(writer.get_line_index_with_content('Break here'), filename=str(original_file))
+        json_facade.write_launch(justMyCode=False, resolveSymlinks=resolve_symlinks)
+        json_facade.write_make_initial_run()
+
+        json_hit = json_facade.wait_for_thread_stopped()
+        filename = json_hit.stack_trace_response.body.stackFrames[0]['source']['path']
+        if resolve_symlinks:
+            assert filename == str(original_file)
+        else:
+            assert filename == str(symlinked_file)
+        json_facade.write_continue()
+        writer.finished_ok = True
+
+
 if __name__ == '__main__':
     pytest.main(['-k', 'test_replace_process', '-s'])
 
diff --git a/tests/debugpy/common/test_messaging.py b/tests/debugpy/common/test_messaging.py
index 7ba88e88..335a5ead 100644
--- a/tests/debugpy/common/test_messaging.py
+++ b/tests/debugpy/common/test_messaging.py
@@ -656,3 +656,21 @@ class TestJsonMessageChannel(object):
         assert fuzzer2.sent == fuzzer1.received
         assert fuzzer1.responses_sent == fuzzer2.responses_received
         assert fuzzer2.responses_sent == fuzzer1.responses_received
+
+
+class TestTypeConversion(object):
+    def test_str_to_num(self):
+
+        # test conversion that are expected to work
+        correct_trials = [("1.0", float), ("1", int), ("1", bool)]
+        for val_trial, type_trial in correct_trials:
+            assert isinstance(
+                json.of_type(type_trial)(val_trial), type_trial
+            ), "Wrong type coversion"
+
+        # test conversion that are not expected to work
+        try:
+            json.of_type(int)("1.0")
+            raise ValueError("This test should have failed")
+        except TypeError:
+            pass
diff --git a/tests/timeline.md b/tests/timeline.md
index ff13a138..8b77e30e 100644
--- a/tests/timeline.md
+++ b/tests/timeline.md
@@ -71,9 +71,9 @@ the return value of `wait_until_realized()` is the first occurrence that realize
 
 ### Basic expectations
 
-A *basic* expectation is described by the circumstances of the occurrence the expectation is to be realized (`expectation.circumstances`). Whereas the circumstances of an occurrence is a data object, the circumstances of the expectation is a *pattern*, as represented by a `Pattern` object from the `pattern` module. An expectation is realized by an occurrence if `occurrence.circumstances in expectation.circumstances` is true (where `in` is an overloaded operator of the `Pattern` object that is used to match values against it; see the docstrings for the `pattern` module for details on patterns). For example, given a basic expectation with these circumstances:
+A *basic* expectation is described by the circumstances of the occurrence the expectation is to be realized (`expectation.circumstances`). Whereas the circumstances of an occurrence is a data object, the circumstances of the expectation is usually a *pattern*, as represented by a `Some` object from the `patterns` package. An expectation is realized by an occurrence if `occurrence.circumstances == expectation.circumstances` is true (for patterns, the overloaded `==` operator is used for matching rather than equality; see the docstrings for the `patterns` package for details). For example, given a basic expectation with these circumstances:
 ```py
-('Event', ANY, some.dict.containing({'threadId': 1}))
+('Event', some.thing, some.dict.containing({'threadId': 1}))
 ```
 It can be realized by any of these occurrences:
 ```py
@@ -96,7 +96,7 @@ Event(event, body)              # Expectation('Event', event, body)
 Mark(id)                        # Expectation('Mark', id)
 ```
 
-For responses, it is often desirable to specify success or failure in general, without details. This can be done by using `some.exception` in the pattern:
+For responses, it is often desirable to specify success or failure in general, without details. This can be done by using `some.error` in the pattern:
 ```py
 Response(request, some.error)
 Response(request, ~some.error)  # success
@@ -191,7 +191,7 @@ assert (
 
 When two expectations are conjuncted: `(A & B)` - the resulting expectation is realized at the occurrence at which `A` and `B` are both realized, regardless of their relative order. Thus:
 ```py
-Event('stopped', ANY) & Event('continued', ANY)
+Event('stopped', some.thing) & Event('continued', some.thing)
 ```
 this expectation will be realized in timeline:
 ```py
@@ -216,7 +216,7 @@ debug_session.wait_until_realized(Response(pause1) & Response(pause2))
 
 When two expectations are disjuncted: `(A | B)` - the resulting expectation is realized at the first occurrence at which either `A` or `B` is realized, or both are realized. Thus, the expectation:
 ```py
-Event('stopped', ANY) | Event('continued', ANY)
+Event('stopped', some.thing) | Event('continued', some.thing)
 ```
 will be realized in any of these timelines:
 ```py