TestExecWindow: TestExecWin - an extension for Visual Studio 2022/2019/2017/2015

TestExecWindow
TestExecWin - an extension for Visual Studio 2022/2019/2017/2015

[Download and Installation] [User guide] [Internal Design]

Abstract

Every time you change the startup project within your Visual Studio solution TestExecWin will automatically check if there are any test cases to run. All found test cases are presented within the TestExecWindow and can there be selected for execution.

Features

  • support for BOOST and (proprietary) TTB testing frameworks
  • gives overview of available test suites / test cases
  • executes test cases by automatically providing suitable start arguments
  • test output is written to a special tab within Visual Studio output pane
  • allows debugging for selected test case
  • jumps to protocol file (.out file)
  • jumps to source code of selected test case
  • allows iteration over all test cases with automatic check if each test case can be run alone
  • supports batch functionality to execute an arbitrary sequence of test executables
  • optional: check for memory leaks
  • optional: automatic build before selected test is started
  • optional: endless repeated execution of test app until (sporadic) failure occurs
  • supports automatic shutdown of computer when last test has terminated
  • possibilty to dock/undock as any other VS window
  • layout may be adjusted to the needs of your project

Online documentation

PPT presentation Start powerpoint web presentation

TestExecWin online documentation:
Here you can first read detailed documentation without need for any download or installation

TestExecWin Windows help file:
Ready to use documentation with full search functionality. Before using you should download the file and copy it to a local hard drive to avoid problems with security settings. If main content is still invisible open properties window within file explorer and allow acccess to downloaded file.

TestExecWin at Visual Studio Marketplace (Version for VS 2022, version for VS 2019, version for VS 2017, version for VS 2015):
Short description and possibility for download.

Contents

Download and Installation

User guide

  • Open TestExecWindow
  • User interface - description of control elements
  • Batch file support
  • More settings - defaults args and configuration of layout
  • Advances settings - automatic build of solution, repeated run until failure, ...
  • Typical test activities
  • Parsing of source files - current limitations

Internal Design

  • Creating a Visual Studio Extension
  • Class TestExecWin_VS2022Package - Visual Studio integration, enabling DTE access
  • Class VisualStudioConnector - Visual Studio: data access via DTE, notifications, automation commands
  • Class SourceFileParser - look for test macros within files of project dir
  • Class Executor - process execution and redirection of output
  • Common functionality for an extension project - Online help, manifest, icons
  • Do not forget to deal with multi threading
  • Source files
Generated on Fri Jan 5 2024 15:38:02 for TestExecWindow by doxygen 1.8.17


TestExecWindow: Modules

TestExecWindow
Modules
Here is a list of all modules:
[detail level 123]
Download and Installation
▼ User guide
Open TestExecWindow
User interface - description of control elements
Batch file support
More settings - defaults args and configuration of layout
Advances settings - automatic build of solution, repeated run until failure, ...
Typical test activities
Parsing of source files - current limitations
▼ Internal Design
Creating a Visual Studio Extension
Class TestExecWin_VS2022Package - Visual Studio integration, enabling DTE access
Class VisualStudioConnector - Visual Studio: data access via DTE, notifications, automation commands
Class SourceFileParser - look for test macros within files of project dir
Class Executor - process execution and redirection of output
Common functionality for an extension project - Online help, manifest, icons
Do not forget to deal with multi threading
▼ Source files
Executor.cs
SourceFileParser.cs
MyToolWindow.cs
MyToolWindowControl.xaml
MyToolWindowControl.xaml.cs
MyToolWindowCommand.cs
TestExecWin_VS2022Package.cs
VisualStudioConnector.cs
Hidden page files of documentation


TestExecWindow: Download and Installation

TestExecWindow
Download and Installation

Download

TestExecWindow is available for free download:

  • VSIX installation file which automatically installs TestExecWin to the specific Visual Studio version:
    TestExecWin_2022.7.3.vsix for Visual Studio 2022
    TestExecWin_2022.7.2.vsix for Visual Studio 2022
    TestExecWin_2019.6.5.vsix for Visual Studio 2019
    TestExecWin_2017.6.4.vsix for Visual Studio 2017
    TestExecWin_1.6.3.vsix for Visual Studio 2015
  • TestExecWin at Visual Studio Marketplace with short description and possibility for download:
    Version for VS 2022
    Version for VS 2019
    Version for VS 2017
    Version for VS 2015
  • Online documentation of newest version for instant reading:
    TestExecWin online documentation
  • Complete C# source code, mark down text files and documentation generated with DoxyGen, packed as zip file:
    TestExecWin_2022_SourcesAndDocumentation.zip:
    TestExecWin_2019_SourcesAndDocumentation.zip:
    Visual Studio 2017: TestExecWin_Sources.zip:
    Visual Studio 2015: TestExecWin_Sources.zip:

For more info about recent changes, known problems and planned enhancements see

  • Change History
  • Features planned for future versions

Installation

  • Within your old version of "TestExecWindow" export your configured sets of test apps if desired.
  • Within Visual studio deinstall your old version of "TestExecWindow" if already existing.
  • Close Visual Studio.
  • Double-Click downloaded installation file "TestExecWin.vsix". Confirm to install the extension.
  • Restart Visual Studio.
  • Within the newly installed version of "TestExecWindow" you now can import the saved sets of test apps.

Remark:
During installation files will be copied to C:/Users /[UserName] /AppData /Local /Microsoft /VisualStudio /14.0 /Extensions /[random name like 'sbmfwobd'].wfs. In case of problems deinstallation can be done by simply removing this directory.

Change History TestExecWin for Visual Studio 2022

Actual version is 2022.7.3

  • Version 2022.7.3 (01/2024)
    • Download TestExecWin_2022.7.3.vsix
    • extended functionality: (optional) Run until failure
      You can execute your test app repeatedly until a failure occurs. Then you have the chance to examine the test protocol and traces of a failed execution.
    • extended functionality: BOOST test cases not belonging to a BOOST test suite are displayed as members of virtual test suite "[no-test-suite]"
    • extended functionality: added support for BOOST macros BOOST_DATA_TEST_CASE, BOOST_DATA_TEST_CASE_F and corresponding TTB macro TTB_BOOST_DATA_TEST_CASE
    • bugfix: an unmatched occurrence of BOOST_AUTO_TEST_SUITE_END caused incomplete parsing of test cases; now such lines will be ignored and a warning will be written to output pane
  • Version 2022.7.2 (02/2022)
    • Download TestExecWin_2022.7.2.vsix
    • adjusted for Visual Studio 2022, using actualized extension framework
    • extended functionality: (optional) Build solution before starting test
      You have not to wait until build is completed before you can start your test manually. Simply edit and save your new code and push "Run" within TestExecWindow. If compile fails then test will not be started. Only if compile succeeds the selected test cases are executed.
    • extended functionality: optional limit CPU for StdOut
      If your test executable produces bursts of messages to stdout the Visual Studio GUI sometimes seem to hang. It is now possible to activate a yield for the thread receiving the output burst. A typical output burst may be caused by memory leak messages at program termination.
    • Default args: added proposal "--log_sink =<PATH_TO_FILE>" for BOOST tests

Change History TestExecWin for Visual Studio 2019

Actual version is 2019.6.5

  • Version 2019.6.5 (03/2019)
    • Download TestExecWin_2019.6.5.vsix
    • adjusted for Visual Studio 2019, now using "AsyncPackage" which supports asynchronous loading

Change History TestExecWin for Visual Studio 2017

Actual version is 2017.6.4

  • Version 2017.6.4 (10/2018)
    • Download TestExecWin_2017.6.4.vsix
    • extended functionality: now supporting macros BOOST_FIXTURE_TEST_CASE, TTB_BOOST_FIXTURE_TEST_CASE
    • change: new default for memory leak checking is "switched off"
    • bugfix: improved the way how the correct project configuration (VCConfiguration) is found; this solves following problems:
      • "Import from solution" did not import all relevant projects from solution
      • when opening Visual Studio directly by clicking on a solution file the current startup project could not be found
    • bugfix: log files generated with option "shutdown" were not saved to target directory but to one directory level higher; when building log file path a missing path separator "\" was added
  • Version 2017.6.3 (12/2017)
    • Download TestExecWin_2017.6.3.vsix
    • has the same functionality as TestExecWin 1.6.3 for Visual Studio 2015

Change History TestExecWin for Visual Studio 2015

Actual version is 1.6.3. Older versions are no longer available online.

  • Version 1.6.3 (12/2017)
    • Download TestExecWin_1.6.3.vsix:
    • improvement for automatic shutdown
      • now using a forced shutdown (option "-f") to allow automatic shutdown also when the desktop is locked
  • Version 1.6.2 (10/2017)
    • improvement for test execution: log files and automatic shutdown
      • you can decide to automatically shutdown your computer when all tests have terminated
      • before shutting down results from log pane are automatically written to logfile
      • logfiles are stored within target directory of current startup project or within temp directory
  • Version 1.6.1 (05/2017)
    • improvement for test execution: max execution time
      • You can optionally define a max wait time interval. When time expires and the current call to your test has not yet terminated the test executable will be killed. The same timeout settings applies to all calls to your test app(s). When running a set of calls (e.g. within the tab with configured test apps or when choosing "Run each" for your test suites) after having killed the (possibly hanging) test app the next call will be started automatically.
    • improvement for list of test groups and funcs:
      • instead of selecting only a single list entry you now can select multiple entries. When clicking "Run selected" all selected test cases/suites will be executed within a single call to your test application.
    • improvement for configured test apps:
      • on button click you can scan your current solution and automatically add all found executables with their full execution path to the currently displayed test app set
      • on button click you can copy the test suites from current startup project to the currently displayed test app set. When executing the test app set each of the imported test suites will be executed within a separate call of your test executable (similar as you have pushed "Run each" within test group tab)
      • on button click you can copy the visible test cases from startup project to the currently displayed test app set. When executing the test app set each of the imported test cases will be executed within a separate call of your test executable (similar as you have pushed "Run each" within test func tab)
      • within your configured sets of test apps you can use "//" comments to (temporarily) skip an app from execution or to add arbitrary notes
      • when exporting a set of test apps the name of the set is used as default file name
      • you can now store 8 sets with test apps instead of 3
    • improvement for BOOST-Test:
      • added support for macro BOOST_AUTO_TEST_CASE_TEMPLATE
    • improvement for log list with test results:
      • copy to clip board (e.g. for sending within Email)
      • export as regular text file
    • improvement for memory check:
      • a string of "Object dump complete" within test output is also interpreted as memory leak. motivation: in case of really huge leak reports the limited buffer of output pane will no longer contain the initial message "Detected memory leaks"
    • bugfix source file parsser: now macro TTB_TEST_FUNC_DESC will be correctly parsed. You can also use "," and ")" within your test case description
  • Version 1.6 (04/2017)
    • new feature: added second register card to support testing of a configurable set of test apps. You can define and save 3 different named sets of test apps directly within plugin settings. Additionally you can export and import your app sets (e.g. into next version of this plugin).
    • improvement: optionally checks for memory leaks (i.e. checks for text "Detected memory leaks" within test output as written to Visual Studio output pane). Only works for properly configured test applications which have activated a final memory check by C runtime. You can switch off this check, if you are tired of solving leak errors.
    • improvement: when started again, automatically restores last settings (e.g. last default args, last used set of test apps, last visibility of GUI controls)
    • improvement: added support for nested BOOST test suites
    • bugfix: test macros (e.g. BOOST_AUTO_TEST_CASE) which are commented out with "//" are ignored now
  • Version 1.5.1 (12/2016)
    • bugfix: input text within default args field and selection of several lists were reset when window was moved, docked or reopened
    • improvement: When being opened TestExecWin now automatically checks for the current startup project and displays all found test cases.
  • Version 1.5 (12/2016)
    • Added "Src" buttons to directly jump to source code location of selected test function or test group
    • Added additional proposals for default args supporting BOOST and TTB test frames
  • Version 1.4 (11/2016)
    • Running test application via Run button now causes dynamic color state for project name instead of changing background color of Run button
    • bugfix: changing startup project while test is running will now be ignored to avoid inconsistent data state and a possible crash of Visual Studio

Features planned for future versions

  • Currently there are no features planned. Let me know if you have special wishes which may be also useful for others.

Next step: Open TestExecWindow


TestExecWindow: User guide

TestExecWindow
Modules
User guide

Modules

Open TestExecWindow
User interface - description of control elements
Batch file support
More settings - defaults args and configuration of layout
Advances settings - automatic build of solution, repeated run until failure, ...
Typical test activities
Parsing of source files - current limitations

Detailed Description

Generated on Fri Jan 5 2024 15:38:02 for TestExecWindow by doxygen 1.8.17


TestExecWindow: Open TestExecWindow

TestExecWindow
Open TestExecWindow
User guide

Open the installed window via Menu View / Other windows / TestExecWindow:

You can use the window as a free floating window or choose any pane within Visual Studio you find suited for attaching it.

  • Read next chapter: User interface - description of control elements


TestExecWindow: User interface - description of control elements

TestExecWindow
User interface - description of control elements
User guide

Depending on your test application TestExecWin will show up similar to the following screenshot:

Upper Row: Current startup project

TestXmlCheckWithPugiXml - Debug
Name of current startup project and currently selected configuration

Checkbox "Debug"
Choose whether to start test within debugger or to execute it regularly

Run
Run all tests; test application is started and only default args (see More settings - defaults args and configuration of layout) are used

Out
Open test protocol file (assumption: YourTestApp.exe generates protocol file YourTestApp.out within target directory)

Refresh
Manually refresh of all data (e.g. when you have just added new test cases to your source code)

Button "?"
Starts your default web browser and opens a short documentation containing links to the full online documentation

Button "..."
Show/hide additional settings (see More settings - defaults args and configuration of layout)

Middle part: Test suites and test cases

The left / right list contains all BOOST test suites / test cases defined in the current startup project.

Check box "within suite"
You can decide whether to show all BOOST test cases or only those contained within the selected BOOST test suite. Test cases not belonging to a BOOST test suite are displayed as members of virtual test suite "[no-test-suite]"

Colored status fields "FAILED/OK"
Execution status of the last test execution

Text field "--runTest="
Command line parameter which is automatically set when selecting one or more items within the list above. You can manually edit the parameter (e.g. for adding additional options)

Run selected
starts the test executable by using the commmand line above. You can select one or more test suites / test cases to be executed within a single call to your test executable

Run each
sequentially executes all test suites / test cases within the list above. Each list entry is executed as a separate call to your test executable. The result of each test run is displayed within log pane.

Src
opens the corresponding source file and jumps to the selected BOOST test case. For Boost test suites within the left list the first BOOST test case within the suite will be presented.

Lower part: Log pane

Log pane containing important status messages. The test execution itself is displayed within the regular output pane of Visual Studio.

Checkbox "Shutdown"
When activated the termination of the selected tests will cause a saving of the log pane to a protocol file and a subsequent shutdown of your computer. The log file will be stored within the target directory of your startup application or (if there is no startup project selected) within the first temporary directory as found within environment variables TMP, TEMP or USERPROFILE.

Clear log
Clear entries within log pane

Copy
Copy list contents to paste buffer for later insertion as regular multiline text.

Export
Export the list contents to a text file.

  • Read next chapter: Batch file support
  • Read previous chapter: Open TestExecWindow


TestExecWindow: Batch file support

TestExecWindow
Batch file support
User guide

To control multiple executions of arbitrary test applications (e.g. the same test app with different start arguments or all/some test apps from your test solution) use batch file feature on tab "Test Apps":

Edit box
Basically the edit box in the middle of the screen has a simple batch syntax which can be used for manual editing:

  • empty lines and lines starting with "//" are ignored
  • all other lines are expected to contain "[fullPathToTestExecutable] [any number of start args]"

Combobox "Test Success"
selects one of 8 predefined batch sets which are stored internally within tool settings. The name of each batch set can be changed by editing the text within the combo box (pay attention to auto switch feature of combo box when starting to edit)

Run all
Starts the execution of all entries within the batch above.

Save
Saves the last changes to the batch above within internal storage.

Reload
Restores the batch to the state last saved to internal storage. May be useful when you want to cancel manual changes.

Clear
Empties the text field.

Export/Import
supports

  • saving to an external text file
  • reading contents from an external text file, the current contents are not replaced, the external contents are inserted at end of current batch
  • automatically importing all test executables from within your current test solution
  • automatically importing all test suites or test cases from the current startup project (e.g. to verify that all tests will run independently from each other or to identify which test cases have problems like memory leaks)
  • Read next chapter: More settings - defaults args and configuration of layout
  • Read previous chapter: User interface - description of control elements


TestExecWindow: More settings - defaults args and configuration of layout

TestExecWindow
More settings - defaults args and configuration of layout
User guide

You can adjust TestExecWindow to your needs. More options for individual configuration are available via the button "..." in the upper right corner. Selecting it leads to a display of the following options:

Default args
define default args, which are used in addition to the args used for selecting a test suite or test case. This may be useful to activate some test behaviour common for all test suites or test cases.

List "only test groups"
decide whether to see both test suites and contained test cases or restrict visibility to one of them

List "sort as read"
choose an appropriate sort order

List "hide log"
hide the log pane if not needed

List "wait for test app max 10 min"
wait indefinitely for termination of a test or set one of several predefined time intervals

List "leak check"
activate or deactivate checking for memory leaks. The leak check assumes that your test app will use elementary memory leak detection facilities of C runtime ("_CrtSetDbgFlag(.._CRTDBG_LEAK_CHECK_DF)"). In case of leaks corresponding messages are written to stdout/stderr ("Detected memory leaks..."). TestExecWin will simply check test output from Visual Studio pane to decide whether leaks are existing or not. In combination with a separate run of each testcase you may identify the evil test cases.

List "regular output"
rise output level within log pane (may be useful when looking for problems)

When the changed options from above are hidden again the configured window will look like:

  • Read next chapter: Advances settings - automatic build of solution, repeated run until failure, ...
  • Read previous chapter: Batch file support


TestExecWindow: Advances settings - automatic build of solution, repeated run until failure, ...

TestExecWindow
Advances settings - automatic build of solution, repeated run until failure, ...
User guide

Advanced options

Additional options are available within the options section of Visual Studio. Navigate to menu "Tools" (or "Extras") / "Options" and then within the list on left side look for entry "TestExecWindow".

Remark:
If there are no entries visible on the right side, please close the option window again and reopen it. Now the entries should be visible. This seems to be a bug in Visual Studio and may depend on the version you have installed.

Option: Build solution before starting test
If this option is active, then each time when you push one of the "Run" buttons within TestExecWindow first the current solution is built. If build fails then the test will not be started. Only if build succeeds the selected test cases are executed.

This has the following advantages:

  • after code changes you cannot forget to build before starting a test
  • you have not to wait until build is completed before you can start your test manually. Simply edit and save your new code and then push "Run" within TestExecWindow.

You may have reasons for not activating this option:

  • you don't want a build of the whole solution before starting a test, because the test needs only a small part of the projects within the solution
  • even in case of no code changes Visual Studio may need a too long time span for checking build dependencies and finally deciding that no build action is needed.

In both cases you can think of using smaller test solutions where only such code is contained which is related to a specific topic or to a limited set of SW modules.

Option: Limit CPU for StdOut
If your test executable produces bursts of messages to stdout (which are directed to log pane of TestExecWindow) the Visual Studio GUI sometimes seem to hang. It is now possible to activate a yield for the thread receiving the output burst. A typical output burst may be caused by memory leak messages at program termination.

Option: Run until failure
Sometimes your test app may have sporadic failures. With activating this option your app will be executed repeatedly until a failure occurs (i.e. your test app returns an exit code different from 0). Now you have the chance to examine the test protocol and traces of a failed execution.

Option: Command line args
This option sets the default command line args which can also be set directly within options of tool window as described above.

Option: Show more options within tool window
This option controls the visibility of the options pane within tool window. The visibility can also be controlled by the buttons "..." and "Hide" within tool window as described above.

  • Read next chapter: Typical test activities
  • Read previous chapter: More settings - defaults args and configuration of layout


TestExecWindow: Typical test activities

TestExecWindow
Typical test activities
User guide

Overview:

  • List available test cases and select for execution
  • Searching for errors, extending test cases
  • Batch testing and automatic shutdown
  • Ignoring memory leaks

List available test cases and select for execution

Typically you will use TestExecWindow to get an overview of available test cases contained within a test application. By selecting a single test suite or a single test case and then pushing "Run selected" TestExecWindow will automatically set appropriate command line parameters and start the corresponding test case.

It depends on the structure of your specific test application if test suites or test cases can be executed independently from each other. If this is not the case you should think about improving your test application. A good advise could be to ensure that at least each test suite could be executed alone.

TestExecWindow supports you to check which test suites or test cases can already run independently. By pushing "Run each" TestExecWindow will start your test executable separately for each item in the list of test suites or test cases. The result of each call to your test application is displayed within the log pane together with the used command line params.

Remark:
If you simply want to execute all test cases as quick as possible it is recommended to use button "Run" at the top of the window. In this case your test application will be started only once to execute all tests.

Searching for errors, extending test cases

If you want to extend your test cases or you want to search the reason for some test failure the following features may be useful:

  • Protocol file
    Pushing "Out" opens the protocol file of your test application within Visual Studio. Usually you could find more detailed information in this file which may allow a better understanding of the error situation. It depends on your specific test application whether there exists a protocol file. TestExecWindow assumes a protocol file with the name "YourTestApp.out" besides the test executable "YourTestApp.exe".
  • Debugging
    When searching for an error it may be useful to start test execution within debugger. You can simply switch debugging on and off by selecting / deselecting checkbox "Debug".
    Remark: The command line params used to execute your test within debugger are stored within the project settings and will not be restored to the original values. You may have to reset them manually when running the debugger outside of TestExecWindow.
  • Src - jump to source code
    Pushing "Src" immediately opens the selected test function or test group within your source code. This may be useful for inspecting the test instructions when looking for a test error or for adding a new test case before or after the selected one.
  • Refresh
    If you have just added a new test case press "Refresh" to trigger a refresh of the list of available test cases. This may also be necessary when you have opened TestExecWindow after you have already loaded your test project. TestExecWindow currentlly listens only to notifications about a changed startup project or a change of Debug/Release. If TestExecWindow is not opened it cannot listen to such events.

Batch testing and automatic shutdown

After some time or when you are working within a team there may be a greater number of test applications and test cases. It is a good idea to execute all unit tests within the nightly or weekly build you may have introduced. You should care for a sufficient level of test output or a detailed test protocol to have enough information for later analysis of possible errors.

TestExecWin also offers the possibility to execute an arbitrary set of test applications in batch mode (for details see Batch file support). This feature may be useful in the following situations:

  • execute a specific subset of tests with one button click
    As a starting point for your batch you may use "Import from solution" to get a complete list of test executables from your test solution.
  • verify that all your test suites can be executed alone
    Use "Import test suites from startup project" to get a list of separate calls to your startup project with appropriate start args to select each test suite.

  • verify that all your test applications are free of memory leaks
    Ensure that within detailed settings "leak check" is activated. The status of each test execution is written to log pane and has one of the following values:

    • OK:
      correct test result, no memory leak detected
      Warning: if your test application is not configured to write memory leaks to stdout or if you have activated "no leak check" within detailed settings then you may get this status despite of having memory leaks!
    • FAILED:
      there are functional errors (e.g. a BOOST-Check has failed), in this case the check for memory leaks was skipped
    • FAILED (MEM_LEAKS):
      correct test result but memory leaks were detected

    Precondition:
    Your test application is configured to write memory leak messages to stdout when program terminates (C runtime supports leak check, see for keywords "_CrtSetDbgFlag", "_CRTDBG_LEAK_CHECK_DF").

  • automatic shutdown and log file
    Before leaving your office you can start a test sequence. If you activate checkbox "Shutdown" your computer will be switched off when all tests have terminated. The next day you can find the test results from log pane within a protocol file with the name "TestExecWin.[InfoAboutExecutedTests].[TimeStamp].Log.txt". The protocol file is located within the target directory of your startup project. If there was no startup project selected at time of execution (via batch feature) you should check your temporary directory as defined via environment variables TMP, TEMP or USERPROFILE.

Ignoring memory leaks

Sometimes you may want to extend or repair functional test cases and you don't want to solve memory leaks also occuring during testing. Possibly circumstances:

  • a new component of someone else (which is part of your test environment) has leaks and he will repair them later.
  • you want to focus on functionality and you currently have no time to analyze leaks

But the leaks may cause the following problems when executing test cases:

  • at end of your test app a big amount of leak messages is written to std::out and you have trouble finding the test results you are interested in
  • the writing of leak messages consumes a remarkable amount of time and disturbs your development of test cases

You must understand that the suppressing of memory leak detection has two steps:

  • Step 1: Control your test app to avoid printing leak messages to std::out
    • For BOOST test apps
      set command line argument "--detect_memory_leak=0"
    • For TTB test apps without BOOST
      set command line argument "-checkForMemLeaks OFF"
    • For all other test apps:
      Memory leak messages usually are created by C runtime support for leak check. You have to change the correspondig settings within your test app (search for keywords "_CrtSetDbgFlag", "_CRTDBG_LEAK_CHECK_DF").
  • Step 2: Control settings for Visual Studio extension "TestExecWin"
    • Setting option "no leak check" (see More settings - defaults args and configuration of layout) will stop analyzing the leak check messages within std::out. A test app with error free test cases but generating leak messages will be regarded as successful (signalled with green color within tool window)
    • The option "no leak check" has no influence on the generation of leak messages itself. This has to be done within the test app itself or within its command line params (see above)
    • Remark: If you have switched off generation of leak messages for your test app then there is no need to switch off option "leak check" of extension "TestExecWin"
  • Read next chapter: Parsing of source files - current limitations
  • Read previous chapter: More settings - defaults args and configuration of layout


TestExecWindow: Parsing of source files - current limitations

TestExecWindow
Parsing of source files - current limitations
User guide

TestExecWindow parses the source files of your test application to look for test suites and test cases to execute. During parsing the following rules and assumptions apply:

  • within your Visual Studio solution only one startup project is set
  • TestExecWindow knows the path to your startup project file. It assumes that all .cpp files located in the project directory or within a subdirectory of it may contain test cases.
  • BOOST tests are expected to use the following macros to define the test case structure:
    BOOST_AUTO_TEST_CASE
    BOOST_AUTO_TEST_CASE_TEMPLATE
    BOOST_AUTO_TEST_SUITE
    BOOST_DATA_TEST_CASE
    BOOST_DATA_TEST_CASE_F
    BOOST_FIXTURE_TEST_SUITE
    BOOST_FIXTURE_TEST_CASE
    BOOST_AUTO_TEST_SUITE_END
    TTB_BOOST_TEST_CASE (a combination of BOOST.Test with proprietary TTB framework)
    TTB_BOOST_DATA_TEST_CASE (a combination of BOOST.Test with proprietary TTB framework)
    TTB_BOOST_FIXTURE_TEST_CASE (a combination of BOOST.Test with proprietary TTB framework)
  • TTB tests are expected to use the following macros to define the test case structure:
    TTB_TEST_FUNC
    TTB_TEST_FUNC_DESC
  • Read previous chapter: Typical test activities


TestExecWindow: Internal Design

TestExecWindow
Modules
Internal Design

Modules

Creating a Visual Studio Extension
Class TestExecWin_VS2022Package - Visual Studio integration, enabling DTE access
Class VisualStudioConnector - Visual Studio: data access via DTE, notifications, automation commands
Class SourceFileParser - look for test macros within files of project dir
Class Executor - process execution and redirection of output
Common functionality for an extension project - Online help, manifest, icons
Do not forget to deal with multi threading
Source files

Detailed Description

  • Creating a Visual Studio Extension
    How to use the Visual Studio Wizard to create an appropriate C# project for writing an extension plus several links to online resoures.
  • Class TestExecWin_VS2022Package - Visual Studio integration, enabling DTE access
    Class TestExecWin_VS2022Package is responsible for registering TestExecWin as a valid package for Visual Studio. Furthermore the class establishes acess to DTE (development tools environment).
  • Class VisualStudioConnector - Visual Studio: data access via DTE, notifications, automation commands
    Class VisualStudioConnector encapsulates (nearly) all communication with Visual Studio. This includes requesting of project properties of current startup project and getting notified about changes within Visual Studio.
  • Class SourceFileParser - look for test macros within files of project dir
    Class SourceFileParser parses all .cpp files within the directory of the current startup project and all of its sub directories. Any found test cases of type BOOST or TTB will be extracted and prepared for presentation at GUI.
  • Class Executor - process execution and redirection of output
    Class Executor supports starting of a test application. Arbitrary start arguments can be passed to the test executable. The output of the test process is redirected to a special output pane within Visual Studio. When the test process terminates Executor will send a notification to TestExecWin to allow proper GUI updates.
  • Class MyToolWindow
    Class MyToolWindow is the main class which instantiates the helper classes VisualStudioConnector, SourceFileParser and Executor and establishes all required connections between them. Furthermore the utility function WriteLine which allows writing to log pane of TestExecWin is implemented here. To reduce coupling TestExecWindow implements the interfaces IMainEvents and IEventReceiver which may be called by other classes.
  • Class MyToolWindowControl within MyToolWindowControl.xaml.cs
    is the typical GUI control class containing all button handlers and all code to manipulate GUI controls. Corresponding status data are stored to allow proper initialization and refresh of GUI display.
  • MyToolWindowControl.xaml
    is the typical layout file in XAML format. Here you can define all controls and their arrangement on screen. You can either directly edit the XAML file with the text editor or you can move controls from the ToolBox to the design editor and drag them around with the mouse.
  • Common functionality for an extension project - Online help, manifest, icons
    General support for online help, and additional display infos for a properly integrated Visual Studio extension. Typical files for an user interface application.
  • Do not forget to deal with multi threading
    TestExecWindow has to deal with multiple threads. Synchronization is guaranteed by passing execution to main (GUI) thread.


TestExecWindow: Creating a Visual Studio Extension

TestExecWindow
Creating a Visual Studio Extension
Internal Design
  • Creating the Visual Studio project
  • Updating a Visual Studio extension at VS Marketplace
  • General links to more information

Creating the Visual Studio extension project

In the following we assume the creation of a separate new tool window for Visual Studio:

  • create a new VSIX project within Visual Studio (New Project / Visual C# / Extensibility)
  • add a tool window item template (Add / New Item / Visual C# / Extensibility / Custom tool window)
  • the generated files can immediately be built
  • start of debugging causes the start of an experimental instance of Visual Studio where your extension is already installed. In the experimental instance of Visual Studio you can test out the functionality without affecting your Visual Studio installation.
  • your new tool window can be opened via View / Other Windows / YourWindowName
  • the generated VSIX file automatically installs your extension on your computer
  • For more detailed info and a step to step guide see Welcome to VS extensibility

Updating a Visual Studio extension at VS Marketplace

  • Sign in to Visual Studio Marketplace and navigate to "Manage" page. Besides the version entry of your extension click the innocent looking 3 points "...". Within the upcoming context menu select "Edit"
  • On the edit page simply use the first point to upload a version. The version number itself will update automatically within the web page.
  • See also: How to publish an update to visual studio extension

General links to more information

  • Series of Videos by Mads Kristensen, starting with "Welcome to VS extensibility"
  • Visual Studio Extension available at Visual Studio Marketplace: Extensibility Template Pack 2022 by Mads Kristensen, see also on github
  • Rishabh Verma, Visual Studio Extensibility Development, Extending Visual Studio IDE for Productivity, Quality, Tooling and Analysis; Apress 2020

General links to Microsoft (Visual Studio 2015 - 2019)

  • Creating an Extension with a Tool Window
  • Detailed documentation and API reference material for building extensions
  • Extension samples on GitHub
  • Watch videos from the product team on Visual Studio extensibility
  • Install an optional helper tool that adds extra IDE support for extension authors
  • Install an optional helper tool that adds extra IDE support for extension authors
  • How to: Migrate Extensibility Projects to Visual Studio 2017
  • Walkthrough: Publishing a Visual Studio Extension
  • How to upgrade extensions to support Visual Studio 2019
  • Migrate to AsyncPackage
  • Improving the responsiveness


TestExecWindow: Class TestExecWin_VS2022Package - Visual Studio integration, enabling DTE access

TestExecWindow
Class TestExecWin_VS2022Package - Visual Studio integration, enabling DTE access
Internal Design

Overview:

  • Visual Studio integration by deriving from base class 'AsyncPackage'
  • Getting acess to DTE

Visual Studio integration by deriving from base class 'AsyncPackage'

Base class "AsyncPackage" derives from a couple of IAsyncXX interfaces which care for proper integration with Visual Studio. To make it even more simple it is recommended to use the Extensibility Template Pack 2022 by Mads Kristensen. Then you can derive from base class ToolkitPackage which is provided by the extension package and itself derives from AsyncPackage. For a code sample see next section.

Getting acess to DTE

Microsoft Visual Studio allows programmatic access via a special environment, the "DTE" (= development tools environment). The "DTE object" is the root of the automation model for Visual Studio.

To get access to DTE object you can establish a connection within the package class of your C# assembly. The reference to the DTE object can be distributed within your application classes as needed.

public sealed class TestExecWin_VS2022Package : ToolkitPackage
{
protected override async Task InitializeAsync(CancellationToken cancellationToken,
IProgress<ServiceProgressData> progress)
{
await this.RegisterCommandsAsync();
this.RegisterToolWindows(); // => calls MyToolWindow constructor
// Remark: In this specific project some object references are stored within the global struct
// MyGlobals for easy access from any point of code.
// Allow access to package (e.g. for getting options page which is managed by package)
MyGlobals.myPackage = this;
// Establish connection to automation object DTE of Visual Studio
if (MyGlobals.myDTE == null)
{
await JoinableTaskFactory.SwitchToMainThreadAsync();
var dte = await this.GetServiceAsync(typeof(EnvDTE.DTE)) as EnvDTE80.DTE2;
Microsoft.Assumes.Present(dte);
// Pass DTE reference to static data members of other classes
MyGlobals.myDTE = dte;
VisualStudioConnector.dte = dte;
}
}
}


TestExecWindow: Class VisualStudioConnector - Visual Studio: data access via DTE, notifications, automation commands

TestExecWindow
Class VisualStudioConnector - Visual Studio: data access via DTE, notifications, automation commands
Internal Design

Overview:

  • Register for notifications at Visual Studio
  • Receiving notifications from Visual Studio
  • Requesting project information via DTE
  • Automate Visual Studio
    • Open text file
    • Start debugging

Register for notifications at Visual Studio

Derive from notification interfaces:

/// Class derives from notification interfaces to get
/// informed when user performs changes within Visual Studio.
class VisualStudioConnector
: Microsoft.VisualStudio.Shell.Interop.IVsSelectionEvents
, Microsoft.VisualStudio.Shell.Interop.IVsUpdateSolutionEvents

Advise for notifications:

private void ConnectWithVisualStudio()
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
bool connectedToSelectionEvents = false;
bool connectedToUpdateSolutionEvents = false;
// Advise to selection events (e.g. startup project changed)
m_monitorSelection = Microsoft.VisualStudio.Shell.ServiceProvider.GlobalProvider.
GetService(typeof(SVsShellMonitorSelection)) as IVsMonitorSelection;
if (m_monitorSelection != null)
{
m_monitorSelection.AdviseSelectionEvents(
this, out m_selectionEventsCookie);
connectedToSelectionEvents = true;
}
else
{
WriteLine(1, "ConnectWithVisualStudio: GetService(SVsShellMonitorSelection) failed!");
}
// Advise to update solution events (e.g. switched debug/release configuration)
m_solutionBuildManager = Microsoft.VisualStudio.Shell.ServiceProvider.GlobalProvider.
GetService(typeof(SVsSolutionBuildManager)) as IVsSolutionBuildManager;
if (m_solutionBuildManager != null)
{
m_solutionBuildManager.AdviseUpdateSolutionEvents(
this, out m_updateSolutionEventsCookie);
connectedToUpdateSolutionEvents = true;
}
else
{
WriteLine(1, "ConnectWithVisualStudio: GetService(SVsSolutionBuildManager) failed!");
}
if (connectedToSelectionEvents && connectedToUpdateSolutionEvents)
{
WriteLine(1, "ConnectWithVisualStudio succeeded");
}
}
catch (Exception ex)
{
WriteLine(1, "ConnectWithVisualStudio failed: EXCEPTION: " + ex.ToString());
}
}

Unadvise from notifications:

/// Release all connections to Visual Studio.
/// This function is called from Dispose method of main app class TestExecWindow
public void DisconnectFromVisualStudio()
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
if (m_monitorSelection != null && m_selectionEventsCookie != 0)
m_monitorSelection.UnadviseSelectionEvents(m_selectionEventsCookie);
if (m_solutionBuildManager != null && m_updateSolutionEventsCookie != 0)
m_solutionBuildManager.UnadviseUpdateSolutionEvents(m_updateSolutionEventsCookie);
}
catch (Exception ex)
{
WriteLine(1, "DisconnectFromVisualStudio: EXCEPTION: " + ex.ToString());
}
}

Receiving notifications from Visual Studio

Notification about a change of startup project:

// ----- Interface IVsSelectionEvents -----
// Check for notification about changed startup project
int IVsSelectionEvents.OnElementValueChanged(
uint elementid, object varValueOld, object varValueNew)
{
if (elementid == (uint)VSConstants.VSSELELEMID.SEID_StartupProject)
{
// When startup project is set in solution explorer a complete refresh is triggered
if (varValueNew != null)
{
WriteLine(2, "Detected new StartupProject");
m_mainEvents.OnRefreshAll();
}
}
return VSConstants.S_OK;
}
// All other events from IVsSelectionEvents are ignored.
// Eventhandler functions simply return OK status.
int IVsSelectionEvents.OnCmdUIContextChanged(
uint dwCmdUICookie, int fActive)
{
return VSConstants.S_OK;
}
/// ...

Notification about a change of configuration:

// ----- Interface IVsUpdateSolutionEvents -----
int IVsUpdateSolutionEvents.OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
{
WriteLine(2, "Detected OnActiveProjectCfgChange");
m_mainEvents.OnRefreshAll();
return VSConstants.S_OK;
}

Requesting project information via DTE

Get current configuration (e.g Debug or Release)

//Get name of config (e.g. "Debug", "Release")
string configName = dte.Solution.SolutionBuild.ActiveConfiguration.Name;

Get name of startup project

EnvDTE80.SolutionBuild2 sb = (EnvDTE80.SolutionBuild2)dte.Solution.SolutionBuild;
if (sb == null)
{
WriteLine(1, "SolutionBuild is null (checking for startup project is not possible)");
return false;
}
// sb.StartupProjects is an array of project file names. Usually we expect
// only a single project name within the array.
// Name is of the format "..\Test\TestRunner\TestRunner.vcxproj"
if (sb.StartupProjects == null)
{
WriteLine(2, "ReadSettingsOfStartupProject-End: StartupProjects not available");
return false;
}
foreach (String item in (Array)sb.StartupProjects)
{
msg += item;
}
WriteLine(3, "startupProjects=" + msg);
// for further processing extract the name without path and extension
// e.g. nameStartupProject = "TestRunner"
// (extraction code not shown here)

Get startup project interface

// Perform recursive search for the startup project through all solution filters:
EnvDTE.Project startupProj = FindProject(nameStartupProject);

Implementation detail: DTE allows iteration over projects

foreach (EnvDTE.Project project in dte.Solution.Projects)

Implementation detail: Hierarchical recursive search through tree of project items within method FindProjectRecursive:

foreach (EnvDTE.ProjectItem item in project.ProjectItems)
{
EnvDTE.Project realProject = item.Object as EnvDTE.Project;
if (realProject != null)
{
if (realProject.Kind == EnvDTE.Constants.vsProjectKindSolutionItems)
{
EnvDTE.Project projectNextLevel = FindProjectRecursive(
realProject, nameProject);
if (projectNextLevel != null)
{
return projectNextLevel;
}
}
else if (realProject.Name == nameProject)
{
return realProject;
}
}
}

Get name and path of generated executable (depends on previously found startupProj and configName)

// Get full path of executable depending on found
// startup project and configuration (Debug/Release)
Microsoft.VisualStudio.VCProjectEngine.VCProject vcProj = null;
try
{
vcProj = (Microsoft.VisualStudio.VCProjectEngine.VCProject)startupProj.Object;
if (vcProj ==null)
{
WriteLine(1, "Project is not of type VCProject and is not supported!");
return false;
}
}
catch (Exception ex)
{
WriteLine(2, "ReadSettingsOfStartupProject-End: EXCEPTION: " + ex.ToString());
WriteLine(1, "Project is not of type VCProject and is not supported!");
return false;
}
Microsoft.VisualStudio.VCProjectEngine.IVCCollection configs =
(Microsoft.VisualStudio.VCProjectEngine.IVCCollection)vcProj.Configurations;
Microsoft.VisualStudio.VCProjectEngine.VCConfiguration config =
FindConfig(configs, configName, m_platFormStartupProject);
if (config == null)
{
WriteLine(1, "Config " + configName + " not found");
return false;
}
msg = "PrimaryOutput (FullExePath)=" + config.PrimaryOutput;
WriteLine(2, msg);

Helper function to find a given configuration

private Microsoft.VisualStudio.VCProjectEngine.VCConfiguration FindConfig(Microsoft.VisualStudio.VCProjectEngine.IVCCollection in_configurations, String in_configName, String in_platform)
{
string expectedConfigName = in_configName + "|" + in_platform;
WriteLine(3, "FindConfig: searching for config " + expectedConfigName);
if ((in_configurations == null) || (in_configurations.Count <= 0))
{
WriteLine(3, "FindConfig: in_configurations is null or empty");
return null;
}
WriteLine(3, "FindConfig: configurations.Count=" + in_configurations.Count);
foreach (Microsoft.VisualStudio.VCProjectEngine.VCConfiguration configItem in in_configurations)
{
WriteLine(3, "FindConfig: configItem.Name=>" + configItem.Name + "< .ConfigurationName=>" + configItem.ConfigurationName);
if (configItem.Name == expectedConfigName)
{
return configItem;
}
}
return null;
}

Automate Visual Studio

Open a text file within Visual Studio

public void OpenFile(string in_fullFilePath, int in_lineNum = 0)
{
WriteLine(3, "OpenFile " + in_fullFilePath + " line " + in_lineNum);
try
{
ThreadHelper.ThrowIfNotOnUIThread();
dte.ItemOperations.OpenFile(in_fullFilePath, EnvDTE.Constants.vsViewKindTextView);
if (in_lineNum>0)
{
dte.ExecuteCommand("Edit.Goto",in_lineNum.ToString());
}
}
catch (Exception ex)
{
WriteLine(1, "Could not open file " + in_fullFilePath + "\nEXCEPTION: " + ex.ToString());
}
}

Start debugging with special command line options

public void StartDebugging(string in_cmdLineParams)
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
if (m_startupProj != null)
{
string cmdLineParams = "";
if (in_cmdLineParams != null)
{
cmdLineParams = in_cmdLineParams;
}
//m_startupProj.ConfigurationManager.ActiveConfiguration.Properties.
// Item("CommandArguments").Value = cmdLineParams;
string configName = dte.Solution.SolutionBuild.ActiveConfiguration.Name;
Microsoft.VisualStudio.VCProjectEngine.VCProject vcProj =
(Microsoft.VisualStudio.VCProjectEngine.VCProject)m_startupProj.Object;
Microsoft.VisualStudio.VCProjectEngine.IVCCollection configs =
(Microsoft.VisualStudio.VCProjectEngine.IVCCollection)vcProj.Configurations;
Microsoft.VisualStudio.VCProjectEngine.VCConfiguration config =
FindConfig(configs, configName, m_platFormStartupProject);
Microsoft.VisualStudio.VCProjectEngine.VCDebugSettings dbgSettings =
(Microsoft.VisualStudio.VCProjectEngine.VCDebugSettings)config.DebugSettings;
dbgSettings.CommandArguments = cmdLineParams;
WriteLine(2, "StartDebugging: now starting debugger with dbgSettings.CommandArguments=" + dbgSettings.CommandArguments);
dte.Debugger.Go(false /* do not wait for end of debugging*/);
}
}
catch (Exception ex)
{
WriteLine(1, "Could not start debugging\nEXCEPTION: " + ex.ToString());
}
}


TestExecWindow: Class SourceFileParser - look for test macros within files of project dir

TestExecWindow
Class SourceFileParser - look for test macros within files of project dir
Internal Design

Overview:

  • Scan project dir for .cpp files
  • Parsing a source file
  • Storing info for each test case

Scan project dir for .cpp files

public void ScanProjectDir(ProjectInfo projectInfo)
{
WriteLine(3, "ScanProjectDir-Begin");
// Recursively scan for .cpp files in project dir and all sub directories
System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(projectInfo.sourceDirPath);
var files = dir.GetFiles("*.cpp", System.IO.SearchOption.AllDirectories);
foreach (var file in files)
{
// Search for test macros and store them within "projectInfo"
ParseFile(projectInfo, file);
}
WriteLine(3, "ScanProjectDir-End");
}

Parsing a source file

During parsing a hard coded set of test macros is searched. Depending on the type of found macro the relevant test case information is extracted at different positions within the macro:

private void ParseFile(ProjectInfo projectInfo, System.IO.FileInfo in_file)
{
WriteLine(3, "ParseFile-Begin " + in_file.FullName);
WriteLine(2, "File " + in_file.FullName);
// Read whole source file into array of strings
string[] lines = System.IO.File.ReadAllLines(in_file.FullName);
int numLines = lines.Length;
// During parsing we store information about found description,
// test suite and current line index within ParseInfo
ParseInfo parseInfo = new ParseInfo(in_file.FullName);
while (parseInfo.lineIdx < numLines)
{
string line = lines[parseInfo.lineIdx];
if (LineContainsMacro("TTB_TEST_FUNC_DESC",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_TEST_FUNC_DESC", line, lines);
if (macro != null)
{
parseInfo.description = macro.param2;
projectInfo.appType = AppType.TTB;
parseInfo.RemoveAllTestGroups();
parseInfo.AddTestGroup(in_file.Name);
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("TTB_TEST_FUNC",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_TEST_FUNC", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.TTB;
parseInfo.RemoveAllTestGroups();
parseInfo.AddTestGroup(in_file.Name);
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_AUTO_TEST_CASE_TEMPLATE", line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_AUTO_TEST_CASE_TEMPLATE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
// Add "*" for correct call of test case
parseInfo.description += "*";
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_AUTO_TEST_CASE",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_AUTO_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if(LineContainsMacro("TTB_BOOST_TEST_CASE",line))
/// ...

Storing info for each test case

private void CreateTestFuncInfo(ProjectInfo projectInfo, ParseInfo parseInfo)
{
// Store the found test group if not already existing
string[] groupNames = parseInfo.GetTestGroupHierarchyString().Split(new Char[] { '/'}, StringSplitOptions.RemoveEmptyEntries);
string groupHierarchyString = "";
List<string> groups = new System.Collections.Generic.List<string>();
if (groupNames.Length == 0)
{
if (!projectInfo.testFuncWithoutTestGroupFound)
{
projectInfo.testFuncWithoutTestGroupFound = true;
groups.Add(ProjectInfo.NO_TESTSUITE_STR);
TestGroupEntry tg = new TestGroupEntry(projectInfo.appType == AppType.BOOST);
tg.fileFullPath = parseInfo.fileFullPath;
tg.lineNum = parseInfo.GetLineNum();
tg.AddTestGroupHierarchy(groups);
projectInfo.testGroups.Add(tg);
}
}
else
{
foreach (string grp in groupNames)
{
groups.Add(grp);
groupHierarchyString += (groupHierarchyString.Length > 0 ? "/" : "");
groupHierarchyString += grp;
if (projectInfo.testGroups.Find(x => (x.GetTestGroupHierarchyString() == groupHierarchyString)) == null)
{
TestGroupEntry tg = new TestGroupEntry(projectInfo.appType == AppType.BOOST);
tg.fileFullPath = parseInfo.fileFullPath;
tg.lineNum = parseInfo.GetLineNum();
tg.AddTestGroupHierarchy(groups);
projectInfo.testGroups.Add(tg);
}
}
}
// Store the found test function description
TestFuncEntry tf = new TestFuncEntry(projectInfo.appType == AppType.BOOST);
tf.testFunc = parseInfo.description;
tf.fileFullPath = parseInfo.fileFullPath;
tf.lineNum = parseInfo.GetLineNum();
tf.AddTestGroupHierarchy(parseInfo.testGroups);
projectInfo.testFuncs.Add(tf);
WriteLine(3, "CreateTestFuncInfo: " + tf.testFunc + " within group: " + tf.GetTestGroupHierarchyString()
+ " file: " + tf.fileFullPath + " line: " + tf.lineNum);
}


TestExecWindow: Class Executor - process execution and redirection of output

TestExecWindow
Class Executor - process execution and redirection of output
Internal Design

Overview:

  • Relevant class attributes
  • Starting a process
  • Redirection of process output
  • Kill a running process

Relevant class attributes

To manage the execution of an external process the following class data are used:

// Control data used for executing an external process
private System.Diagnostics.Process m_process = null;
// Flag indicating that the process has been started
private bool m_processStarted = false;
// Output window pane within Visual Studio to be
// used as stdout for test executable
private EnvDTE.OutputWindowPane m_outputPane = null;
// Flag indicating whether stdout and stderr
// of the started executable shall be redirected and
// a separate execution window shall be suppressed.
bool m_catchStdOutAndStdErr = true;
// Flag indicating whether Executor shall be notified
// when the process terminates
bool m_getNotificationWhenProcessTerminates = true;
// Flag indicating whether after process termination
// the process output is checked for string "Detected memory leaks"
private bool m_checkForMemoryLeaks = false;

Starting a process

public void StartProcess(string exePath, string args, string workDir)
{
try
{
string argsToUse = "";
if (args != null)
{
argsToUse = args;
}
ThreadHelper.ThrowIfNotOnUIThread();
// Ensure executable exists
if (!System.IO.File.Exists(exePath))
{
m_mainEvents.OnTestTerminated(false, exePath, false, new TimeSpan(0));
WriteLine(1, "Executable not found: " + exePath);
//m_eventReceiver.MsgBox("Executable not found: " + exePath);
return;
}
// Ensure output pane exists
if (m_outputPane == null)
{
EnvDTE.OutputWindow ow = MyGlobals.myDTE.ToolWindows.OutputWindow;
m_outputPane = ow.OutputWindowPanes.Add("Run UnitTest");
}
m_outputPane.Activate();
m_outputPane.Clear();
// ----- Prepare process data -----
m_process = new System.Diagnostics.Process();
m_process.StartInfo.FileName = exePath;
m_process.StartInfo.WorkingDirectory = workDir;
m_process.StartInfo.Arguments = argsToUse.Trim();
if (m_getNotificationWhenProcessTerminates)
{
// Remark: Method Executor.Process_Exited will be called
// from some system thread when the process has terminated.
// Synchronization will be needed!
m_process.Exited += new System.EventHandler(Process_Exited);
m_process.EnableRaisingEvents = true;
}
if (m_catchStdOutAndStdErr)
{
m_process.StartInfo.UseShellExecute = false;
m_process.StartInfo.RedirectStandardOutput = true;
m_process.StartInfo.RedirectStandardError = true;
m_process.StartInfo.CreateNoWindow = true;
m_process.OutputDataReceived += new System.Diagnostics.
DataReceivedEventHandler(StandardOutputReceiver);
m_process.ErrorDataReceived += new System.Diagnostics.
DataReceivedEventHandler(StandardErrorReceiver);
}
// ----- Start the new process and start redirection of output -----
m_process.Start();
m_processStarted = true;
if (m_catchStdOutAndStdErr)
{
m_process.BeginOutputReadLine();
m_process.BeginErrorReadLine();
}
WriteLine(2, "Started " + m_process.StartInfo.FileName);
}
catch (Exception e)
{
string info = "EXCEPTION: Could not start executable " + exePath + " " + e.ToString();
WriteLine(1, info);
m_mainEvents.OnTestTerminated(false, exePath, false, new TimeSpan(0));
}
}

Redirection of process output

The methods StandardOutputReceiver and StandardErrorReceiver will receive any output from the started process. On each call they will pass the given data to the output pane of Visual Studio:

private void StandardOutputReceiver(object sendingProcess,
System.Diagnostics.DataReceivedEventArgs outLine)
{
// Receives the child process' standard output within any system thread
// Give time slice to other threads to keep VS GUI responsive in case of output bursts
// (e.g. memory leak messages at termination of program)
if (Services.GetOptions().LimitCpuForStdOut)
{
System.Threading.Thread.Yield();
}
if (!string.IsNullOrEmpty(outLine.Data))
{
// Delegate execution to main (GUI) thread
Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.Run(async delegate
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
if (m_outputPane != null)
m_outputPane.OutputString(outLine.Data + System.Environment.NewLine);
});
}
}

Kill a running process

A running process can be stopped at any time by simply calling:

m_process.Kill();

Even with killing the process the callback function Executor.Process_Exited will be called.


TestExecWindow: Common functionality for an extension project - Online help, manifest, icons

TestExecWindow
Common functionality for an extension project - Online help, manifest, icons
Internal Design

Overview:

  • Index.html - Providing online help
  • Manifest with key infos for the user
  • Icons for a tool window

Index.html - Providing online help

This file is used to generate online help which will be available within Visual Studio under Menu "Extensions / Manage Extensions" when TestExecWin is selected within the list of tools and when you click on "First steps".

You can provide any HTML coded textual information together with embedded images. The image files should be stored within sub directory "Resources" and added as existing items to the equally named project filter.

It is required that the picture files will be included into the generated VSIX package otherwise the corresponding HTML links will fail for the installed extension. Therefore the corresponding settings of each file have to be set:

  • Include in VSIX: True
  • Copy in output directory: copy always

Screenshot of settingsdialog:

Manifest with key infos for the user

The generated file "source.extension.vsixmanifest" is a manifest file which contains key infos which are displayed to the user when your extension is selected under Menu "Extras / Extension and Updates" or when the user starts installation of the generated VSIX package.

Within Visual Studio an edit mask is displayed to enter relevant information:

Icons for a tool window

Within Visual Studio menu "Extras / Extension and Updates" a big icon is displayed. Use the icon Resurces/YourApp.ico as generated by the wizard. If needed adjust it to size 32 x 32 pixel to get a bigger picture.

Example TestExecWin.ico:

Within menu "View / Other windows" a smaller icon is displayed. It is taken from the most left part of the wizard generated bitmap Resources/YourAppCommand.png. Adjust this bitmap to your needs.

Example TestExecWindowCommand.png:


TestExecWindow: Do not forget to deal with multi threading

TestExecWindow
Do not forget to deal with multi threading
Internal Design

Overview:

  • The problem - unknown threads may call
  • A simple solution - synchronize via GUI thread

The problem - unknown threads may call

Within TestExecWindow many interactions start with pushing a button at the GUI. Those actions are executed within the safe main GUI thread (dispatcher thread).

But we also react on asynhronous events which will arrive on some notification thread:

  • When the test executable terminates we get informed by a system thread which is different from our GUI thread.
  • Visual Studio sends notifications when something has changed (e.g. new startup project). Currently it seems that those notifications will already arrive on the safe GUI thread. But can we be sure for future versions?

Critical situations may occur when data are accessed both from GUI thread and some notification thread. Unsynchronized changes may lead to corrupt data and program crash. When concerning GUI elements (buttons, list boxes etc.) access is only allowed from GUI thread. Visual Studio will rise an exception when access is tried from some other thread.

A simple solution - always synchronize with main thread

Assume we want to write a message to the log pane by calling the service function MyToolWindow.WriteLine() from any other source file. We cannot be sure which thread will call this function. The service function calls method AddInfoToEventList() within GUI class MyToolWindowControl.xaml.cs where we can pass the call to the main thread:

public void AddInfoToEventList(string info)
{
// Redirect to main GUI thread
Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.Run(async delegate {
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
AddInfoToEventListFromGuiThread(info);
});
}
private void AddInfoToEventListFromGuiThread(string info)
{
// here we are sure to be within GUI thread and we can
// safely access the list box
lstEvents.Items.Add(info);
/// ...

An analogous problem occurs when we receive the message about the end of the test process within main class MyToolWindow. In this case we also use the main thread for synchronization:

public void OnTestTerminated(bool in_success, string in_args, bool in_memLeaksDetected, TimeSpan in_executionTime)
{
// This function is called when the test app terminates.
// The call may arrive on any system thread and needs to be synchronized with the GUI thread.
Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.Run(async delegate
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Gui().TestTerminatedWithinGuiThread(in_success, "", in_args, in_memLeaksDetected, in_executionTime);
});
}


TestExecWindow: Source files

TestExecWindow
Modules
Source files
Internal Design

Modules

Executor.cs
SourceFileParser.cs
MyToolWindow.cs
MyToolWindowControl.xaml
MyToolWindowControl.xaml.cs
MyToolWindowCommand.cs
TestExecWin_VS2022Package.cs
VisualStudioConnector.cs
Hidden page files of documentation

Detailed Description

Generated on Fri Jan 5 2024 15:38:02 for TestExecWindow by doxygen 1.8.17


TestExecWindow: Executor.cs

TestExecWindow
Executor.cs
Internal Design » Source files
//------------------------------------------------------------------------------
// Copyright(C) 2022 Gerald Fahrnholz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// Contact: http://www.gerald-fahrnholz.eu/impressum.php
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TestExecWin_VS2022
{
class Executor : IExecute
{
private IMainEvents m_mainEvents;
private IEventReceiver m_eventReceiver;
/// [exec data]
// Control data used for executing an external process
private System.Diagnostics.Process m_process = null;
// Flag indicating that the process has been started
private bool m_processStarted = false;
// Output window pane within Visual Studio to be
// used as stdout for test executable
private EnvDTE.OutputWindowPane m_outputPane = null;
// Flag indicating whether stdout and stderr
// of the started executable shall be redirected and
// a separate execution window shall be suppressed.
bool m_catchStdOutAndStdErr = true;
// Flag indicating whether Executor shall be notified
// when the process terminates
bool m_getNotificationWhenProcessTerminates = true;
// Flag indicating whether after process termination
// the process output is checked for string "Detected memory leaks"
private bool m_checkForMemoryLeaks = false;
/// [exec data]
public Executor(IMainEvents in_mainEvents, IEventReceiver in_eventReceiver)
{
m_mainEvents = in_mainEvents;
m_eventReceiver = in_eventReceiver;
}
public void SetMemLeakCheck(bool in_state)
{
m_checkForMemoryLeaks = in_state;
}
/// [exec start process]
public void StartProcess(string exePath, string args, string workDir)
{
try
{
string argsToUse = "";
if (args != null)
{
argsToUse = args;
}
ThreadHelper.ThrowIfNotOnUIThread();
// Ensure executable exists
if (!System.IO.File.Exists(exePath))
{
m_mainEvents.OnTestTerminated(false, exePath, false, new TimeSpan(0));
WriteLine(1, "Executable not found: " + exePath);
//m_eventReceiver.MsgBox("Executable not found: " + exePath);
return;
}
// Ensure output pane exists
if (m_outputPane == null)
{
EnvDTE.OutputWindow ow = MyGlobals.myDTE.ToolWindows.OutputWindow;
m_outputPane = ow.OutputWindowPanes.Add("Run UnitTest");
}
m_outputPane.Activate();
m_outputPane.Clear();
// ----- Prepare process data -----
m_process = new System.Diagnostics.Process();
m_process.StartInfo.FileName = exePath;
m_process.StartInfo.WorkingDirectory = workDir;
m_process.StartInfo.Arguments = argsToUse.Trim();
if (m_getNotificationWhenProcessTerminates)
{
// Remark: Method Executor.Process_Exited will be called
// from some system thread when the process has terminated.
// Synchronization will be needed!
m_process.Exited += new System.EventHandler(Process_Exited);
m_process.EnableRaisingEvents = true;
}
if (m_catchStdOutAndStdErr)
{
m_process.StartInfo.UseShellExecute = false;
m_process.StartInfo.RedirectStandardOutput = true;
m_process.StartInfo.RedirectStandardError = true;
m_process.StartInfo.CreateNoWindow = true;
m_process.OutputDataReceived += new System.Diagnostics.
DataReceivedEventHandler(StandardOutputReceiver);
m_process.ErrorDataReceived += new System.Diagnostics.
DataReceivedEventHandler(StandardErrorReceiver);
}
// ----- Start the new process and start redirection of output -----
m_process.Start();
m_processStarted = true;
if (m_catchStdOutAndStdErr)
{
m_process.BeginOutputReadLine();
m_process.BeginErrorReadLine();
}
WriteLine(2, "Started " + m_process.StartInfo.FileName);
}
catch (Exception e)
{
string info = "EXCEPTION: Could not start executable " + exePath + " " + e.ToString();
WriteLine(1, info);
m_mainEvents.OnTestTerminated(false, exePath, false, new TimeSpan(0));
}
}
/// [exec start process]
// Handle Exited event and display process information.
public void Process_Exited(object sender, System.EventArgs e)
{
m_processStarted = false;
string info = string.Format("Exit time: {0}" +
" Exit code: {1}", m_process.ExitTime, m_process.ExitCode);
TimeSpan executionTime = m_process.ExitTime - m_process.StartTime;
WriteLine(2, info);
System.Threading.Thread.Sleep(500); // wait needed for completion of output (e.g. memory leak messages)
// This function is called when the test app terminates.
// The call may arrive on any system thread and needs to be synchronized with the GUI thread.
Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.Run(async delegate
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Process_Exited_MainThread(executionTime);
});
}
private void Process_Exited_MainThread(TimeSpan executionTime)
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
bool memLeaksDetected = false;
if (m_checkForMemoryLeaks)
{
WriteLine(3, "Process_Exited_MainThread: Checking mem leaks...");
var sel = m_outputPane.TextDocument.Selection;
WriteLine(3, "Process_Exited_MainThread: selection acquired");
sel.SelectAll();
WriteLine(3, "Process_Exited_MainThread: all selected");
//sel.StartOfDocument(false);
//sel.EndOfDocument(true);
//System.IO.File.WriteAllText("C:\\MyTemp\\OutputPane.txt", sel.Text);
// In case of many memory leaks the output containing "Detected memory leaks" may
// no longer be within limited buffer of output pane. Therefore check also for
// final message "Object dump complete".
if ((sel.Text.Contains("Detected memory leaks")) || (sel.Text.Contains("Object dump complete")))
{
memLeaksDetected = true;
WriteLine(3, "Process_Exited_MainThread: ERROR: Memory leaks detected!");
}
}
m_mainEvents.OnTestTerminated(m_process.ExitCode == 0, m_process.StartInfo.Arguments, memLeaksDetected, executionTime);
}
catch (Exception)
{
string info = "EXCEPTION within Process_Exited: "/* + e.ToString()*/;
WriteLine(1, info);
m_mainEvents.OnTestTerminated(false, m_process.StartInfo.Arguments, false, new TimeSpan(0));
}
}
public void KillRunningProcess()
{
try
{
if (!m_processStarted)
{
WriteLine(2, "Executor: There is no process to kill!");
return;
}
WriteLine(1, "Killing " + m_process.ProcessName);
/// [exec kill process]
m_process.Kill();
/// [exec kill process]
}
catch (Exception e)
{
string info = "EXCEPTION: " + e.ToString();
WriteLine(2, info);
m_mainEvents.OnTestTerminated(false, m_process.StartInfo.Arguments, false, new TimeSpan(0));
}
}
/// [exec redirect stdout]
private void StandardOutputReceiver(object sendingProcess,
System.Diagnostics.DataReceivedEventArgs outLine)
{
// Receives the child process' standard output within any system thread
// Give time slice to other threads to keep VS GUI responsive in case of output bursts
// (e.g. memory leak messages at termination of program)
if (Services.GetOptions().LimitCpuForStdOut)
{
System.Threading.Thread.Yield();
}
if (!string.IsNullOrEmpty(outLine.Data))
{
// Delegate execution to main (GUI) thread
Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.Run(async delegate
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
if (m_outputPane != null)
m_outputPane.OutputString(outLine.Data + System.Environment.NewLine);
});
}
}
/// [exec redirect stdout]
private void StandardErrorReceiver(object sendingProcess,
System.Diagnostics.DataReceivedEventArgs errLine)
{
// Give time slice to other threads (e.g. VS GUI)
if (Services.GetOptions().LimitCpuForStdOut)
{
System.Threading.Thread.Yield();
}
// Receives the child process' standard error
if (!string.IsNullOrEmpty(errLine.Data))
{
Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.Run(async delegate
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
if (m_outputPane != null)
m_outputPane.OutputString("Error> " + errLine.Data + System.Environment.NewLine);
});
}
}
private void WriteLine(int in_outputLevel, String in_info)
{
m_eventReceiver.WriteLine(in_outputLevel, in_info);
}
}
}


TestExecWindow: SourceFileParser.cs

TestExecWindow
SourceFileParser.cs
Internal Design » Source files
//------------------------------------------------------------------------------
// Copyright(C) 2022 Gerald Fahrnholz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// Contact: http://www.gerald-fahrnholz.eu/impressum.php
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TestExecWin_VS2022
{
public class TokenInfo
{
public string token { get; set; } // description read as last from source file
public bool exists { get; set; }
public int startIdx { get; set; }
public int endIdx { get; set; }
public bool fatalError { get; set; }
public TokenInfo()
{
token = "";
exists = false;
startIdx = -1;
endIdx = -1;
fatalError = false;
}
public void NotFound(int in_startIdx)
{
token = "";
exists = false;
startIdx = in_startIdx;
endIdx = -1;
}
}
public class MacroInfo
{
public string param1 { get; set; }
public string param2 { get; set; }
public MacroInfo(string in_param1, string in_param2)
{
param1 = in_param1;
param2 = in_param2;
}
}
public class ParseInfo
{
public string description { get; set; } // description read as last from source file
public string fileFullPath { get; set; }
public int lineIdx { get; set; }
public System.Collections.Generic.List<String> testGroups; // BOOST: hierarchical nested set of test suites, TTB: source file name
public ParseInfo(string in_fileFullPath)
{
description = "";
fileFullPath = in_fileFullPath;
lineIdx = 0;
testGroups = new System.Collections.Generic.List<string>(); // hierarchy of test groups
}
public int GetLineNum()
{
return lineIdx + 1;
}
public void AddTestGroup(string in_testGroup)
{
testGroups.Add(in_testGroup);
}
public void RemoveLastTestGroup()
{
testGroups.RemoveAt(testGroups.Count-1);
}
public void RemoveAllTestGroups()
{
testGroups.Clear();
}
public string GetTestGroupHierarchyString()
{
string allTestGroups = "";
foreach (string grp in testGroups)
{
if (allTestGroups != "")
{
allTestGroups += "/";
}
allTestGroups += grp;
}
return allTestGroups;
}
}
class SourceFileParser
{
private readonly IEventReceiver m_eventReceiver;
public SourceFileParser(IEventReceiver in_eventReceiver)
{
m_eventReceiver = in_eventReceiver;
}
/// [parser_scan_dir]
public void ScanProjectDir(ProjectInfo projectInfo)
{
WriteLine(3, "ScanProjectDir-Begin");
// Recursively scan for .cpp files in project dir and all sub directories
System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(projectInfo.sourceDirPath);
var files = dir.GetFiles("*.cpp", System.IO.SearchOption.AllDirectories);
foreach (var file in files)
{
// Search for test macros and store them within "projectInfo"
ParseFile(projectInfo, file);
}
WriteLine(3, "ScanProjectDir-End");
}
/// [parser_scan_dir]
private bool LineContainsMacro(string in_macro, string line)
{
int posStartMacro = line.IndexOf(in_macro);
if (posStartMacro >= 0) // macro found
{
int posStartComment = line.IndexOf("//");
if ((posStartComment >= 0) && (posStartComment < posStartMacro))
{
WriteLine(3, "LineContainsMacro: macro " + in_macro + " within comment is ignored");
return false;
}
return true;
}
return false;
}
/// [parser_read_file]
private void ParseFile(ProjectInfo projectInfo, System.IO.FileInfo in_file)
{
WriteLine(3, "ParseFile-Begin " + in_file.FullName);
WriteLine(2, "File " + in_file.FullName);
// Read whole source file into array of strings
string[] lines = System.IO.File.ReadAllLines(in_file.FullName);
int numLines = lines.Length;
// During parsing we store information about found description,
// test suite and current line index within ParseInfo
ParseInfo parseInfo = new ParseInfo(in_file.FullName);
while (parseInfo.lineIdx < numLines)
{
string line = lines[parseInfo.lineIdx];
if (LineContainsMacro("TTB_TEST_FUNC_DESC",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_TEST_FUNC_DESC", line, lines);
if (macro != null)
{
parseInfo.description = macro.param2;
projectInfo.appType = AppType.TTB;
parseInfo.RemoveAllTestGroups();
parseInfo.AddTestGroup(in_file.Name);
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("TTB_TEST_FUNC",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_TEST_FUNC", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.TTB;
parseInfo.RemoveAllTestGroups();
parseInfo.AddTestGroup(in_file.Name);
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_AUTO_TEST_CASE_TEMPLATE", line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_AUTO_TEST_CASE_TEMPLATE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
// Add "*" for correct call of test case
parseInfo.description += "*";
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_AUTO_TEST_CASE",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_AUTO_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if(LineContainsMacro("TTB_BOOST_TEST_CASE",line))
/// ...
/// [parser_read_file]
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_BOOST_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_FIXTURE_TEST_CASE_TEMPLATE", line)) // also matches TTB_BOOST_FIXTURE_TEST_CASE_TEMPLATE
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_FIXTURE_TEST_CASE_TEMPLATE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1 + "*";
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_DATA_TEST_CASE_F", line)) // also matches TTB_BOOST_DATA_TEST_CASE_F
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_DATA_TEST_CASE_F", line, lines);
if (macro != null)
{
parseInfo.description = macro.param2 + "*";
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_DATA_TEST_CASE", line)) // also matches TTB_BOOST_DATA_TEST_CASE
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_DATA_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1 + "*";
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_FIXTURE_TEST_CASE", line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_FIXTURE_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("TTB_BOOST_FIXTURE_TEST_CASE", line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_BOOST_FIXTURE_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("RUN_TEST_CASE",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_BOOST_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if(LineContainsMacro("BOOST_AUTO_TEST_SUITE_END",line))
{
if (parseInfo.testGroups.Count > 0)
{
parseInfo.RemoveLastTestGroup();
}
else
{
WriteLine(1, "WARNING: Ignoring unbalanced BOOST_AUTO_TEST_SUITE_END in line "
+ parseInfo.GetLineNum() + " of file " + parseInfo.fileFullPath);
}
}
else if (LineContainsMacro("BOOST_AUTO_TEST_SUITE",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_AUTO_TEST_SUITE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
parseInfo.AddTestGroup(parseInfo.description);
}
}
else if (LineContainsMacro("BOOST_FIXTURE_TEST_SUITE",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_FIXTURE_TEST_SUITE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
parseInfo.AddTestGroup(parseInfo.description);
}
}
++parseInfo.lineIdx;
}
WriteLine(3, "ParseFile-End projectInfo.appType=" + projectInfo.appType);
}
private bool ReadNextToken(TokenInfo tokenInfo, string line)
{
tokenInfo.token = "";
// Skip leading white space
int posStart = tokenInfo.startIdx;
while (posStart < line.Length)
{
if ((line[posStart] == ' ') || (line[posStart] =='\t'))
{
++posStart;
}
else
{
break;
}
}
if (posStart >= line.Length)
{
tokenInfo.NotFound(posStart);
return false;
}
// Start reading
bool readString = (line[posStart] == '"');
bool endOfStringReached = false;
tokenInfo.token += line[posStart];
tokenInfo.startIdx = posStart;
int idx = posStart + 1;
if ((line[posStart]==',') || (line[posStart] == ')'))
{
// token is already comolete
tokenInfo.endIdx = posStart;
tokenInfo.exists = true;
return true;
}
else if (readString) // at least another '"' must be found
{
while ((idx < line.Length) && (!endOfStringReached))
{
tokenInfo.token += line[idx];
endOfStringReached = line[idx] == '"';
++idx;
}
if (endOfStringReached)
{
tokenInfo.endIdx = idx-1;
tokenInfo.exists = true;
return true;
}
else // (idx >= line.Length)
{
WriteLine(1, "ERROR: unsupported syntax: string not closed");
tokenInfo.NotFound(posStart);
tokenInfo.fatalError = true;
return false;
}
}
else // read until " ", ",", ")" or end of line is found
{
string endChars = " ,)";
while ((idx < line.Length) && (endChars.IndexOf(line[idx], 0) < 0))
{
tokenInfo.token += line[idx];
++idx;
}
tokenInfo.endIdx = idx - 1;
tokenInfo.exists = true;
return true;
}
}
private string GetContextInfo(ParseInfo parseInfo)
{
return " [" + parseInfo.fileFullPath + " line " + (parseInfo.lineIdx + 1) + "]";
}
// Reads all macros with format: MACRO(), MACRO(param), MACRO(param,param), MACRO( "witin string, you can usse, ()", param)
// White space is automatically skipped, strings containing arbitrary chars are parsed as a single parameter,
// macro is terminated with closing brace ")", macro may have line breaks (up to 3 lines).
private MacroInfo ReadMacro(ProjectInfo projectInfo, ParseInfo parseInfo, string in_macro, string in_line, string[] in_lines)
{
WriteLine(3, "ReadMacro line " + parseInfo.lineIdx + ": " + in_line);
string line = in_line;
int posStart = line.IndexOf(in_macro) + 1;
posStart = line.IndexOf("(", posStart);
if (posStart < 0) // give up
{
WriteLine(1, "WARNING: unsupported syntax: '( 'not found" + GetContextInfo(parseInfo));
return null;
}
TokenInfo tokenInfo = new TokenInfo();
tokenInfo.startIdx = posStart + 1;
// now start reading after the opening brace "("
int maxNumLinesAllowedForMacro = 3;
bool endReached = false;
int paramIdx = 0;
string[] param = new string[] { "", "" };
while (!endReached && !tokenInfo.fatalError && (maxNumLinesAllowedForMacro) > 0)
{
if (ReadNextToken(tokenInfo, line))
{
WriteLine(3, "ReadMacro token= " + tokenInfo.token);
if ((tokenInfo.token != ",") && (tokenInfo.token != ")"))
{
if (paramIdx <= 1)
{
param[paramIdx] = tokenInfo.token;
++paramIdx;
}
else
{
WriteLine(3, "ReadMacro unexpected param is ignored: " + tokenInfo.token + GetContextInfo(parseInfo));
}
}
else
{
endReached = tokenInfo.token == ")";
}
tokenInfo.startIdx = tokenInfo.endIdx + 1;
}
else
{
if (!tokenInfo.fatalError) // check also next line
{
--maxNumLinesAllowedForMacro;
if (maxNumLinesAllowedForMacro > 0)
{
++parseInfo.lineIdx;
if (parseInfo.lineIdx >= in_lines.Length)
{
WriteLine(1, "WARNING: unsupported syntax: end of file reached" + GetContextInfo(parseInfo));
return null;
}
string nextLine = in_lines[parseInfo.lineIdx];
line = line + nextLine;
}
}
}
}
if (!endReached)
{
WriteLine(1, "WARNING: unsupported syntax: end of macro not found" + GetContextInfo(parseInfo));
return null;
}
return new MacroInfo(param[0],param[1]);
}
/// [parser_func_info]
private void CreateTestFuncInfo(ProjectInfo projectInfo, ParseInfo parseInfo)
{
// Store the found test group if not already existing
string[] groupNames = parseInfo.GetTestGroupHierarchyString().Split(new Char[] { '/'}, StringSplitOptions.RemoveEmptyEntries);
string groupHierarchyString = "";
List<string> groups = new System.Collections.Generic.List<string>();
if (groupNames.Length == 0)
{
if (!projectInfo.testFuncWithoutTestGroupFound)
{
projectInfo.testFuncWithoutTestGroupFound = true;
groups.Add(ProjectInfo.NO_TESTSUITE_STR);
TestGroupEntry tg = new TestGroupEntry(projectInfo.appType == AppType.BOOST);
tg.fileFullPath = parseInfo.fileFullPath;
tg.lineNum = parseInfo.GetLineNum();
tg.AddTestGroupHierarchy(groups);
projectInfo.testGroups.Add(tg);
}
}
else
{
foreach (string grp in groupNames)
{
groups.Add(grp);
groupHierarchyString += (groupHierarchyString.Length > 0 ? "/" : "");
groupHierarchyString += grp;
if (projectInfo.testGroups.Find(x => (x.GetTestGroupHierarchyString() == groupHierarchyString)) == null)
{
TestGroupEntry tg = new TestGroupEntry(projectInfo.appType == AppType.BOOST);
tg.fileFullPath = parseInfo.fileFullPath;
tg.lineNum = parseInfo.GetLineNum();
tg.AddTestGroupHierarchy(groups);
projectInfo.testGroups.Add(tg);
}
}
}
// Store the found test function description
TestFuncEntry tf = new TestFuncEntry(projectInfo.appType == AppType.BOOST);
tf.testFunc = parseInfo.description;
tf.fileFullPath = parseInfo.fileFullPath;
tf.lineNum = parseInfo.GetLineNum();
tf.AddTestGroupHierarchy(parseInfo.testGroups);
projectInfo.testFuncs.Add(tf);
WriteLine(3, "CreateTestFuncInfo: " + tf.testFunc + " within group: " + tf.GetTestGroupHierarchyString()
+ " file: " + tf.fileFullPath + " line: " + tf.lineNum);
}
/// [parser_func_info]
private void WriteLine(int in_outputLevel, String in_info)
{
m_eventReceiver.WriteLine(in_outputLevel, in_info);
}
}
}


TestExecWindow: MyToolWindow.cs

TestExecWindow
MyToolWindow.cs
Internal Design » Source files
//------------------------------------------------------------------------------
// Copyright(C) 2022 Gerald Fahrnholz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// Contact: http://www.gerald-fahrnholz.eu/impressum.php
//------------------------------------------------------------------------------
using Microsoft.VisualStudio.Imaging;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace TestExecWin_VS2022
{
public class MyToolWindow : BaseToolWindow<MyToolWindow>, IEventReceiver, IMainEvents
{
// Helper objects with specific tasks
private VisualStudioConnector m_vsConnector;
private SourceFileParser m_parser;
private int m_outputLevel = 1;
private bool m_writeThreadId = false;
private ProjectInfo m_projectInfo = new ProjectInfo();
private bool m_initialMessageWasDisplayed = false;
//----- Construction and initialization -----
public MyToolWindow()
{
// Remark: this method is called from Toolkit.AsyncPackageExtensions.RegisterToolWindows,
// which is triggered by TestExecWin_VS2022Package.InitializeAsync
MyGlobals.myEventReceiver = this;
MyGlobals.myMainEvents = this;
}
public override string GetTitle(int toolWindowId) => "TestExecWindow";
public override Type PaneType => typeof(Pane);
public override Task<FrameworkElement> CreateAsync(int toolWindowId, CancellationToken cancellationToken)
{
// Remark: this method is called from ToolkitPackage.InitializeToolWindowAsync
return Task.FromResult<FrameworkElement>(new MyToolWindowControl());
}
//----- Interface IMainEvents -----
public void OnGuiIsStarted()
{
// Create other classes after GUI instantiation because method "WriteLine" is used
// on many code locations and causes output to log pane
m_vsConnector = new VisualStudioConnector(this, this);
m_parser = new SourceFileParser(this);
MyGlobals.myExecutor = new Executor(this, this);
OnRefreshAll();
}
void IMainEvents.OnSettingsHaveChanged()
{
Gui().SomeSettingsExternallyChanged();
WriteOptions();
}
public void OnRefreshAll()
{
// Synchronize via GUI thread
Gui().TriggerDelayedRefresh();
}
void IMainEvents.OnRefreshNow()
{
// If test is still running we do not want to remove current data
if (Gui().TestIsRunning())
{
string info = "WARNING: Ignoring change of startup project while test is running";
WriteLine(1, info);
return;
}
// We assume to be within safe GUI thread
ThreadHelper.ThrowIfNotOnUIThread();
RefreshAll();
}
void IMainEvents.OnSetOutputLevel(int in_level)
{
m_outputLevel = in_level;
m_writeThreadId = m_outputLevel >= 2;
}
void IMainEvents.OnStartBuilding()
{
ThreadHelper.ThrowIfNotOnUIThread();
m_vsConnector.BuildProject();
}
void IMainEvents.OnBuildTerminated(bool success)
{
ThreadHelper.ThrowIfNotOnUIThread();
Gui().BuildTerminated(success);
}
void IMainEvents.OnStartTest(string exePath, string args, string workDir)
{
MyGlobals.myExecutor.StartProcess(exePath, args, workDir); ;
}
/// [test terminated]
public void OnTestTerminated(bool in_success, string in_args, bool in_memLeaksDetected, TimeSpan in_executionTime)
{
// This function is called when the test app terminates.
// The call may arrive on any system thread and needs to be synchronized with the GUI thread.
Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.Run(async delegate
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Gui().TestTerminatedWithinGuiThread(in_success, "", in_args, in_memLeaksDetected, in_executionTime);
});
}
/// [test terminated]
void IMainEvents.OnStopAll()
{
ThreadHelper.ThrowIfNotOnUIThread();
m_vsConnector.StopBuildingProject();
MyGlobals.myExecutor.KillRunningProcess();
}
void IMainEvents.OnStartDebugging(string in_cmdLineParams)
{
ThreadHelper.ThrowIfNotOnUIThread();
m_vsConnector.StartDebugging(in_cmdLineParams);
}
void IMainEvents.OnOpenProtocolFile()
{
ThreadHelper.ThrowIfNotOnUIThread();
string protocolFilePath = m_projectInfo.GetExePath();
protocolFilePath = protocolFilePath.Replace(".exe", ".out");
if (!System.IO.File.Exists(protocolFilePath))
{
MsgBox("Test protocol not found: " + protocolFilePath);
}
else
{
m_vsConnector.OpenFile(protocolFilePath);
}
}
void IMainEvents.OnOpenSourceFile(string in_fullFileName, int in_lineNum)
{
ThreadHelper.ThrowIfNotOnUIThread();
if (!System.IO.File.Exists(in_fullFileName))
{
MsgBox("Sourcefile not found: " + in_fullFileName);
}
else
{
m_vsConnector.OpenFile(in_fullFileName, in_lineNum);
}
}
string IMainEvents.OnGetExecutablesFromCurrentSolution()
{
ThreadHelper.ThrowIfNotOnUIThread();
return m_vsConnector.GetExecutablesFromCurrentSolution();
}
//----- Interface IEventReceiver -----
public void WriteLine(int in_outputLevel, String in_info)
{
if (in_outputLevel <= m_outputLevel)
{
if (m_writeThreadId)
{
Gui().AddInfoToEventList(Services.GetThreadIdPrompt() + in_info);
}
else
{
Gui().AddInfoToEventList(in_info);
}
}
}
public void MsgBox(string in_info)
{
System.Windows.MessageBox.Show(in_info, "Info");
}
//----- Helper methods -----
private void RefreshAll()
{
WriteLine(3, "RefreshAll-Begin");
try
{
ThreadHelper.ThrowIfNotOnUIThread();
if(!m_initialMessageWasDisplayed)
{
m_initialMessageWasDisplayed = true;
var options = Services.GetOptions();
WriteLine(1, "Options: Build before test=" + options.BuildSolutionBeforeStartingTest.ToString()
+ ", Check for memory leaks = " + options.CheckForMemoryLeaks.ToString());
}
// Reset data
m_projectInfo = new ProjectInfo();
if (!m_vsConnector.ReadSettingsOfStartupProject(m_projectInfo))
{
// MsgBox("There is no startup project defined or the project settings are not accessible!");
WriteLine(2, "WARNING: There is no startup project defined or the project settings are not accessible!");
return;
}
m_parser.ScanProjectDir(m_projectInfo);
Gui().SetTestInfo(m_projectInfo);
WriteLine(1, "Detected project " + m_projectInfo.project + " (" + m_projectInfo.config + ")"
+ ", type is " + m_projectInfo.appType
+ ", " + m_projectInfo.testGroups.Count + " test groups, "
+ m_projectInfo.testFuncs.Count + " test funcs");
}
catch (Exception e)
{
string info = "EXCEPTION: " + e.ToString();
WriteLine(1, info);
}
WriteLine(3, "RefreshAll-End");
}
private void WriteOptions()
{
var options = Services.GetOptions();
WriteLine(2, "Options: Build before test=" + options.BuildSolutionBeforeStartingTest.ToString()
+ ", Check for memory leaks = " + options.CheckForMemoryLeaks.ToString());
WriteLine(2, "Options: Limit CPU for std out =" + options.LimitCpuForStdOut.ToString());
WriteLine(2, "Options: Run until failure =" + options.RunUntilFailure.ToString());
}
MyToolWindowControl Gui()
{
return MyGlobals.myToolWindowControl;
}
[Guid("8b4ef7af-4568-4c07-ba13-89dd69794c1f")]
internal class Pane : ToolWindowPane
{
public Pane()
{
// Remark: code is not used?
// Icon (in color green) is now controlled by old fashioned bitmap
// within VSCommandTable.vsct
BitmapImageMoniker = KnownMonikers.ConsoleTest;
}
}
}
}


TestExecWindow: MyToolWindowControl.xaml

TestExecWindow
MyToolWindowControl.xaml
Internal Design » Source files
<UserControl x:Class="TestExecWin_VS2022.MyToolWindowControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:imaging="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.Imaging"
xmlns:theming="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Imaging"
xmlns:util="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Utilities"
xmlns:catalog="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.ImageCatalog"
xmlns:toolkit="clr-namespace:Community.VisualStudio.Toolkit;assembly=Community.VisualStudio.Toolkit"
Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
Foreground="{DynamicResource VsBrush.WindowText}"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300"
Name="TestExecWindow">
<!-- removed from above: toolkit:Themes.UseVsTheme="True" -->
<Grid x:Name="mainGrid" Margin="5" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition Height="30" x:Name="mainRowDefaultArgs" />
<RowDefinition Height="56" x:Name="mainRowOptions" />
<RowDefinition Height="8*"/>
<RowDefinition Height="1*" x:Name="mainRowLog" />
<RowDefinition Height="30" x:Name="mainRowLogButton"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="mainColGroupsOld" Width="*"/>
<ColumnDefinition x:Name="mainColFuncsOld" Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.ColumnSpan="2" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="txtInfo" Grid.Column="0" HorizontalAlignment="Left" Padding="2,6,2,2" Text="Startup project not set" RenderTransformOrigin="0.492,0.593" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.ColumnSpan="2" >
<CheckBox x:Name="chkUseDebugger" Margin="4,7,4,-5" Content="Debug" Checked="chkUseDebugger_CheckBoxChanged" Unchecked="chkUseDebugger_CheckBoxChanged" VerticalAlignment="Top" Height="23" />
<Button x:Name="btnRunTestAll" Margin="4,0,2,2" Content="Run" Height="21" VerticalAlignment="Bottom" Click="btnRunTestAll_Click"/>
<Button x:Name="btnOpenProtocolFile" Margin="4,0,2,2" Content="Out" Height="21" VerticalAlignment="Bottom" Click="btnOpenProtocolFile_Click" />
<Button x:Name="btnRefreshAll" Margin="4,2,2,2" Content="Refresh" Click="btnRefreshAll_Click"/>
<Button x:Name="btnHelp" Margin="4,2,2,2" Content="?" Click="btnHelp_Click"/>
<Button x:Name="btnMoreOptions" Margin="4,2,2,2" Content="..." Click="btnMoreOptions_Click" />
</StackPanel>
</Grid>
<Grid Grid.Row="1" Grid.ColumnSpan="2" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="66"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="txtInfoDefaultArgs" Grid.Row="0" Grid.Column="0" Margin="0,7" HorizontalAlignment="Left" VerticalAlignment="Center" Grid.ColumnSpan="2" >Default args:</TextBlock>
<ComboBox x:Name="cbxDefaultArgs" Grid.Row="0" Grid.Column="2" Margin="2,4" HorizontalAlignment="Stretch" VerticalAlignment="Center" TextBoxBase.TextChanged="cbxDefaultArgs_TextChanged"/>
</Grid>
<Grid Grid.Row="2" Grid.ColumnSpan="2" Margin="0,2,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition Height="25"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<ComboBox x:Name="cbxColVisibility" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Stretch" SelectionChanged="cbxColVisibility_SelectionChanged" />
<ComboBox x:Name="cbxSortOrder" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Stretch" SelectionChanged="cbxSortOrder_SelectionChanged" />
<ComboBox x:Name="cbxLogVisibility" Grid.Row="0" Grid.Column="2" HorizontalAlignment="Stretch" SelectionChanged="cbxLogVisibility_SelectionChanged" />
<ComboBox x:Name="cbxMaxExecutionTime" Grid.Row="1" Margin="0,0,2,0" Grid.Column="0" HorizontalAlignment="Stretch" SelectionChanged="cbxMaxExecutionTime_SelectionChanged" />
<ComboBox x:Name="cbxMemLeakCheck" Grid.Row="1" Margin="0,0,2,0" Grid.Column="1" HorizontalAlignment="Stretch" SelectionChanged="cbxMemLeakCheck_SelectionChanged" />
<ComboBox x:Name="cbxOutputLevel" Grid.Row="1" Grid.Column="2" HorizontalAlignment="Stretch" SelectionChanged="cbxOutputLevel_SelectionChanged" />
</Grid>
<Grid Grid.Row="3" Grid.ColumnSpan="2" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TabControl x:Name="tabControl" Grid.Row="0" Grid.Column="0" MinWidth="60" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" >
<TabItem Header="Startup project" VerticalAlignment="Bottom">
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="5*" x:Name="mainRowLogNew" />
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="mainColGroups" Width="*"/>
<ColumnDefinition x:Name="mainColFuncs" Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="txtInfoTestGroups" Margin="4" HorizontalAlignment="Left" VerticalAlignment="Bottom" Grid.Row="0" Grid.Column="0">txtInfoTestGroups</TextBlock>
<StackPanel Orientation="Horizontal" Grid.Row="0" Grid.Column="1" >
<TextBlock x:Name="txtInfoTestFuncs" Margin="4" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="Auto" >txtInfoTestFuncs</TextBlock>
<CheckBox x:Name="chkTestFuncsForSelTestGroup" Margin="4" Content="within selected suite/file" HorizontalAlignment="Left" VerticalAlignment="Bottom" Checked="chkTestFuncsForSelTestGroup_CheckBoxChanged" Unchecked="chkTestFuncsForSelTestGroup_CheckBoxChanged" />
</StackPanel>
<ListBox x:Name="lstTestGroups" Margin="4" Grid.Row="1" Grid.Column="0" SelectionChanged="lstTestGroups_SelectionChanged" />
<ListBox x:Name="lstTestFuncs" Margin="4" Grid.Row="1" Grid.Column="1" SelectionChanged="lstTestFuncs_SelectionChanged" />
<TextBlock x:Name="txtInfoStarArgTestGroup" Padding="2" Margin="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="2" Grid.Column="0">Select test file / test suite or edit cmdparam below:</TextBlock>
<TextBlock x:Name="txtInfoStarArgTestFunc" Padding="2" Margin="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="2" Grid.Column="1">Select single test or edit cmd param below:</TextBlock>
<TextBox x:Name="txtStartArgTestGroup" Margin="4" Height="25" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Padding="2" Grid.Row="3" Grid.Column="0" Text="" />
<TextBox x:Name="txtStartArgTestFunc" Margin="4" Height="25" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Padding="2" Grid.Row="3" Grid.Column="1" Text="" />
<Grid Grid.Row="4" Grid.Column="0" Margin="4,2,5,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.45*"/>
<ColumnDefinition Width="0.35*"/>
<ColumnDefinition Width="0.2*"/>
</Grid.ColumnDefinitions>
<Button x:Name="btnRunSelectedTestGroup" Content="Run selected" Margin="0,2,2,2" Grid.Row="0" Grid.Column="0" Click="btnRunSelectedTestGroup_Click"/>
<Button x:Name="btnRunAllTestGroups" Content="Run each" Margin="2,2,2,2" Grid.Row="0" Grid.Column="1" Click="btnRunAllTestGroups_Click" />
<Button x:Name="btnGoToSrcTestGroup" Content="Src" Margin="2,2,0,2" Grid.Row="0" Grid.Column="2" Click="btnGoToSrcTestGroup_Click"/>
</Grid>
<Grid Grid.Row="4" Grid.Column="1" Margin="4,2,4,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.45*"/>
<ColumnDefinition Width="0.35*"/>
<ColumnDefinition Width="0.20*"/>
</Grid.ColumnDefinitions>
<Button x:Name="btnRunSelectedTestFunc" Content="Run selected" Margin="0,2,2,2" Grid.Row="0" Grid.Column="0" Click="btnRunSelectedTestFunc_Click" />
<Button x:Name="btnRunAllTestFuncs" Content="Run each" Margin="2,2,2,2" Grid.Row="0" Grid.Column="1" Click="btnRunAllTestFuncs_Click" />
<Button x:Name="btnGoToSrcTestFunc" Content="Src" Margin="2,2,0,2" Grid.Row="0" Grid.Column="2" Click="btnGoToSrcTestFunc_Click"/>
</Grid>
</Grid>
</TabItem>
<TabItem Header="Test Apps" >
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="1*" />
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="14*"/>
<ColumnDefinition Width="59*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" Margin="0,2,0,0" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ComboBox x:Name="cbxDescTestApps" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Stretch" SelectionChanged="cbxDescTestApps_SelectionChanged" />
<TextBlock x:Name="txtInfoTestApps" Grid.Row="0" Grid.Column="1" Margin="4,4" HorizontalAlignment="Left" VerticalAlignment="Center" Grid.ColumnSpan="2" >&#45;&#45;</TextBlock>
</Grid>
<TextBox x:Name="txtTestApps" Grid.Row="1" Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" TextWrapping="NoWrap" Text="[enter full path of your test apps - each on a separate line]" AcceptsReturn="True" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Grid.ColumnSpan="2" />
<Grid Grid.Row="2" Grid.Column="0" Margin="4,2,4,2" Grid.ColumnSpan="2">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.4*"/>
<ColumnDefinition Width="0.4*"/>
<ColumnDefinition Width="0.4*"/>
<ColumnDefinition Width="0.4*"/>
<ColumnDefinition Width="0.5*"/>
</Grid.ColumnDefinitions>
<Button x:Name="btnRunAllTestApps" Grid.Row="0" Grid.Column="0" Margin="0,2,2,2" Content="Run all" Click="btnRunAllTestApps_Click"/>
<Button x:Name="btnSaveTestApps" Grid.Row="0" Grid.Column="1" Margin="2,2,2,2" Content="Save" Click="btnSaveTestApps_Click"/>
<Button x:Name="btnReloadTestApps" Grid.Row="0" Grid.Column="2" Margin="2,2,2,2" Content="Reload" Click="btnReloadTestApps_Click"/>
<Button x:Name="btnClearTestApps" Grid.Row="0" Grid.Column="3" Margin="2,2,2,2" Content="Clear" Click="btnClearTestApps_Click"/>
<ComboBox x:Name="cbxExportImportTestApps" Grid.Row="0" Grid.Column="4" Margin="2,2,0,2" SelectionChanged="cbxExportImportTestApps_SelectionChangeCommitted" SelectedIndex="0">
<ComboBoxItem Visibility="Collapsed">Export/Import</ComboBoxItem>
<ComboBoxItem>Export to file...</ComboBoxItem>
<ComboBoxItem>Import from file...</ComboBoxItem>
<ComboBoxItem>Import from solution</ComboBoxItem>
<ComboBoxItem>Import test suites from startup project</ComboBoxItem>
<ComboBoxItem>Import visible test cases from startup project</ComboBoxItem>
</ComboBox>
</Grid>
</Grid>
</TabItem>
</TabControl>
</Grid>
<ListBox x:Name="lstEvents" Grid.Row="4" Grid.ColumnSpan="2" Margin="4" />
<Grid Grid.Row="5" Grid.ColumnSpan="2" Margin="4,2,4,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="5*"/>
</Grid.ColumnDefinitions>
<CheckBox x:Name="chkShutdown" Grid.Row="0" Grid.Column="0" MinWidth="60" Margin="0,0,0,-3" Content="Shutdown" Checked="chkShutdown_CheckBoxChanged" Unchecked="chkShutdown_CheckBoxChanged" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Height="23" />
<Button x:Name="btnClearEvents" Grid.Row="0" Grid.Column="1" MinWidth="60" Margin="2,2,4,2" Content="Clear log" Click="btnClearEvents_Click"/>
<Button x:Name="btnCopyLog" Grid.Row="0" Grid.Column="2" MinWidth="60" Margin="2,2,4,2" Content="Copy" Click="btnCopyLog_Click"/>
<Button x:Name="btnExportLog" Grid.Row="0" Grid.Column="3" MinWidth="60" Margin="2,2,4,2" Content="Export..." Click="btnExportLog_Click"/>
</Grid>
</Grid>
</UserControl>


TestExecWindow: MyToolWindowControl.xaml.cs

TestExecWindow
MyToolWindowControl.xaml.cs
Internal Design » Source files
//------------------------------------------------------------------------------
// Copyright(C) 2022 Gerald Fahrnholz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// Contact: http://www.gerald-fahrnholz.eu/impressum.php
//------------------------------------------------------------------------------
using Microsoft.VisualStudio.RpcContracts.FileSystem;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Controls;
namespace TestExecWin_VS2022
{
class MyListItem
{
public int idx;
public string displayString;
public MyListItem(int in_idx, string in_displayString)
{
idx = in_idx;
displayString = in_displayString;
}
public override string ToString()
{
return displayString;
}
};
public partial class MyToolWindowControl : UserControl
{
private ProjectInfo m_projectInfo = new ProjectInfo();
private bool m_useDebugger = false;
enum RunMode { TEST_GROUPS, TEST_FUNCS, TEST_APPS, TEST_STARTUP_PROJECT };
private RunMode m_curRunMode = RunMode.TEST_STARTUP_PROJECT;
private int[] m_numExecutedTests = new int[] { 0, 0, 0, 0 };
private int[] m_numFailedTests = new int[] { 0, 0, 0, 0 };
private int[] m_totalNumTestsToExecute = new int[] { 0, 0, 0, 0 };
private bool m_stoppedByUser = false;
enum WaitMode { WAIT_ENDLESS, WAIT_30_SEC, WAIT_1_MIN, WAIT_2_MIN, WAIT_5_MIN, WAIT_10_MIN, WAIT_20_MIN, WAIT_30_MIN, WAIT_40_MIN, WAIT_60_MIN };
private WaitMode m_waitMode = (WaitMode)Services.GetOptions().IdxMaxWaitTime;
enum ColVisibility { BOTH, LEFT, RIGHT };
ColVisibility m_colVisibility = (ColVisibility)Services.GetOptions().IdxGroupsAndFuncs;
private int m_idxRunAllTestGroups = -1;
private int m_idxRunAllTestFuncs = -1;
private int m_idxRunAllTestApps = -1;
enum ExpImpMode { EXPORT_TO_FILE = 1, IMPPORT_FROM_FILE, IMPORT_FROM_SOLUTION, IMPORT_TEST_GROUPS_FROM_STARTUP_PROJECT, IMPORT_VISIBLE_TEST_FUNCS_FROM_STARTUP_PROJECT };
private List<string> m_currentTestApps = new List<string>();
private List<string> m_dataDescTestApps;
private int m_idxCurrentTestApps = Services.GetOptions().IdxCurrentTestApps;
private readonly System.Windows.Media.Brush m_originalBrushWindow;
private readonly System.Windows.Media.Brush m_originalBrushTextBlock;
private readonly System.Windows.Forms.Timer m_executionTimer = new System.Windows.Forms.Timer();
private readonly System.Windows.Threading.DispatcherTimer m_refreshTimer = new System.Windows.Threading.DispatcherTimer();
private bool m_refreshTimerIsActive = false;
// Is basically a copy of m_projectInfo.testGroups/testFuncs but this copy may be sorted
// according to user requests, The original collection stores the elements in the
// order as read from files.
System.Collections.Generic.List<TestGroupEntry> m_testGroups;
System.Collections.Generic.List<TestFuncEntry> m_testFuncs;
HashSet<string> m_selectedTestGroups;
enum SortOrder { AS_READ_FROM_FILE, REVERSE_READ, ALPHA, REVERSE_ALPHA };
SortOrder m_sortOrder = (SortOrder)Services.GetOptions().IdxSortOrder;
bool m_optionsVisible = Services.GetOptions().OptionsAreVisible;
bool m_logVisible = Services.GetOptions().LogIsVisible;
enum State { IDLE, RUNNING, SUCCEEDED, FAILED };
private State[] m_state = new State[] { State.IDLE, State.IDLE, State.IDLE, State.IDLE };
//----- Construction and initialization -----
public MyToolWindowControl()
{
InitializeComponent();
m_originalBrushWindow = mainGrid.Background;
m_originalBrushTextBlock = txtInfoStarArgTestGroup.Background;
m_executionTimer.Enabled = false;
m_executionTimer.Tick += new EventHandler(OnExecutionTimeout);
for (int i = 0; i < 4; ++i)
{
m_state[i] = State.IDLE;
}
MyGlobals.myToolWindowControl = this;
MyGlobals.myMainEvents.OnGuiIsStarted();
InitControls();
}
private void InitControls()
{
lstTestGroups.SelectionMode = SelectionMode.Extended;
lstTestFuncs.SelectionMode = SelectionMode.Extended;
txtInfoDefaultArgs.Background = m_originalBrushTextBlock;
txtInfoDefaultArgs.Foreground = System.Windows.Media.Brushes.Black;
// Get user options
var options = Services.GetOptions();
{
List<string> data = new List<string>();
data.Add("");
data.Add("BOOST options:");
data.Add("--detect_memory_leak=0");
data.Add("--log_level=test_suite");
data.Add("--log_sink=<PATH_TO_LOGFILE>");
data.Add("");
data.Add("TTB Runner options:");
data.Add("-- -checkForMemLeaks OFF");
data.Add("-- -help");
data.Add("");
data.Add("custom options:");
data.Add("-- -prefixEvents TTB_EXP");
cbxDefaultArgs.ItemsSource = data;
cbxDefaultArgs.SelectedIndex = 0;
cbxDefaultArgs.IsEditable = true;
cbxDefaultArgs.Text = options.StartArgs;
}
{
List<string> data = new List<string>();
data.Add("test groups and funcs");
data.Add("only test groups");
data.Add("only test funcs");
//var comboBox = sender as ComboBox;
cbxColVisibility.ItemsSource = data;
cbxColVisibility.SelectedIndex = (int)m_colVisibility;
}
{
List<string> data = new List<string>();
data.Add("sort as read");
data.Add("reverse read order");
data.Add("sort A->Z");
data.Add("sort Z->A");
cbxSortOrder.ItemsSource = data;
cbxSortOrder.SelectedIndex = (int)m_sortOrder;
}
{
List<string> data = new List<string>();
data.Add("show log");
data.Add("hide log");
cbxLogVisibility.ItemsSource = data;
cbxLogVisibility.SelectedIndex = m_logVisible ? 0 : 1;
}
{
List<string> data = new List<string>();
data.Add("regular output");
data.Add("verbous output");
data.Add("detailed output");
int outputLevel = options.IdxOutputLevel + 1;
cbxOutputLevel.ItemsSource = data;
cbxOutputLevel.SelectedIndex = outputLevel - 1;
MyGlobals.myMainEvents.OnSetOutputLevel(outputLevel);
}
{
List<string> data = new List<string>();
data.Add("leak check");
data.Add("no leak check");
cbxMemLeakCheck.ItemsSource = data;
bool leakCheck = options.CheckForMemoryLeaks;
cbxMemLeakCheck.SelectedIndex = leakCheck ? 0 : 1;
MyGlobals.myExecutor.SetMemLeakCheck(leakCheck);
}
{
List<string> data = new List<string>();
data.Add("wait for test app without time limit");
data.Add("wait for test app max 30 sec");
data.Add("wait for test app max 1 min");
data.Add("wait for test app max 2 min");
data.Add("wait for test app max 5 min");
data.Add("wait for test app max 10 min");
data.Add("wait for test app max 20 min");
data.Add("wait for test app max 30 min");
data.Add("wait for test app max 40 min");
data.Add("wait for test app max 60 min");
cbxMaxExecutionTime.ItemsSource = data;
cbxMaxExecutionTime.SelectedIndex = (int)m_waitMode;
}
{
m_dataDescTestApps = new List<string>();
m_dataDescTestApps.Add(options.GetDescTestAppsForIdx(0));
m_dataDescTestApps.Add(options.GetDescTestAppsForIdx(1));
m_dataDescTestApps.Add(options.GetDescTestAppsForIdx(2));
m_dataDescTestApps.Add(options.GetDescTestAppsForIdx(3));
m_dataDescTestApps.Add(options.GetDescTestAppsForIdx(4));
m_dataDescTestApps.Add(options.GetDescTestAppsForIdx(5));
m_dataDescTestApps.Add(options.GetDescTestAppsForIdx(6));
m_dataDescTestApps.Add(options.GetDescTestAppsForIdx(7));
cbxDescTestApps.ItemsSource = m_dataDescTestApps;
cbxDescTestApps.SelectedIndex = m_idxCurrentTestApps;
cbxDescTestApps.IsEditable = true;
}
}
//----- Called from other classes -----
public void SomeSettingsExternallyChanged()
{
// Redirect to main GUI thread
WriteLine(2, "SettingsExternallyChanged: passing to gui thread...");
Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.Run(async delegate {
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
var options = Services.GetOptions();
cbxDefaultArgs.Text = options.StartArgs;
m_optionsVisible = Services.GetOptions().OptionsAreVisible;
RefreshDisplay();
});
}
public void TriggerDelayedRefresh()
{
// Redirect to main GUI thread
WriteLine(2, "TriggerDelayedRefresh: passing to gui thread...");
Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.Run(async delegate {
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
TriggerDelayedRefreshFromGuiThread();
});
}
internal void SetTestInfo(ProjectInfo in_projectInfo)
{
m_projectInfo = in_projectInfo;
for (int i = 0; i < 4; ++i)
{
m_state[i] = State.IDLE;
m_numFailedTests[i] = 0;
m_numExecutedTests[i] = 0;
m_totalNumTestsToExecute[i] = 0;
}
if (m_projectInfo != null)
{
txtInfo.Text = m_projectInfo.project + " - " + m_projectInfo.config + " " + m_projectInfo.platform;
RefreshTestGroupList();
RefreshTestFuncList();
}
RefreshDisplay();
}
public void BuildTerminated(bool success)
{
if (m_stoppedByUser)
{
WriteLine(1, "Build terminated, success=" + success.ToString() + ", Test is aborted (stopped by user)");
TestTerminatedWithinGuiThread(false, "", "", false, new TimeSpan(0));
return;
}
if (success)
{
WriteLine(2, "Build succeeded, now starting test...");
if (m_curRunMode == RunMode.TEST_STARTUP_PROJECT)
{
StartProcess(m_projectInfo.GetExePath(), cbxDefaultArgs.Text, m_projectInfo.targetDirPath);
}
else if (m_curRunMode == RunMode.TEST_GROUPS)
{
if (m_idxRunAllTestGroups == -1)
{
StartProcess(m_projectInfo.GetExePath(), txtStartArgTestGroup.Text + " " + cbxDefaultArgs.Text, m_projectInfo.targetDirPath);
}
else // start all test groups
{
StartTestGroup(0);
}
}
else if (m_curRunMode == RunMode.TEST_FUNCS)
{
if (m_idxRunAllTestFuncs == -1)
{
StartProcess(m_projectInfo.GetExePath(), txtStartArgTestFunc.Text + " " + cbxDefaultArgs.Text, m_projectInfo.targetDirPath);
}
else // start all test funcs
{
StartTestFunc(0);
}
}
else
{
WriteLine(2, "WARNING: no longer prepared for testing, ignored Build succeeded event!");
}
}
else // build failed, do not start testing
{
if (TestIsRunning())
{
WriteLine(1, "ERROR: Build failed or could not be started, test is aborted!");
SetState(false);
}
else
{
WriteLine(2, "WARNING: no longer prepared for testing, ignored Build failed event!");
}
}
}
//----- EventHandlers for GUI controls -----
private void chkUseDebugger_CheckBoxChanged(object sender, RoutedEventArgs e)
{
try
{
m_useDebugger = (bool)chkUseDebugger.IsChecked;
RefreshState();
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
private void btnRunTestAll_Click(object sender, RoutedEventArgs e)
{
try
{
if (m_useDebugger)
{
MyGlobals.myMainEvents.OnStartDebugging(cbxDefaultArgs.Text);
}
else // regular execution
{
if (m_state[(int)RunMode.TEST_STARTUP_PROJECT] == State.RUNNING)
{
StopExecution();
}
else
{
InitForTestExecution(RunMode.TEST_STARTUP_PROJECT, 1);
RefreshState();
if (Services.GetOptions().BuildSolutionBeforeStartingTest)
{
MyGlobals.myMainEvents.OnStartBuilding();
}
else
{
StartProcess(m_projectInfo.GetExePath(), cbxDefaultArgs.Text, m_projectInfo.targetDirPath);
}
}
}
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
MyGlobals.myMainEvents.OnTestTerminated(false, m_projectInfo.GetExePath(), false, new TimeSpan(0));
}
}
private void btnOpenProtocolFile_Click(object sender, RoutedEventArgs e)
{
MyGlobals.myMainEvents.OnOpenProtocolFile();
}
private void btnRefreshAll_Click(object sender, RoutedEventArgs e)
{
MyGlobals.myMainEvents.OnRefreshNow();
}
private void btnHelp_Click(object sender, RoutedEventArgs e)
{
try
{
string assemblyPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
WriteLine(2, "Info: assemblyPath = " + assemblyPath);
string dirPath = new System.IO.FileInfo(assemblyPath).Directory.FullName;
string pathToHelpFile = dirPath + @"\Resources\index.html";
//string pathToHelpFile = System.Reflection.Assembly.GetExecutingAssembly()..Location + @".index.html";
WriteLine(1, "Trying to open help file " + pathToHelpFile);
// Open page with default web browser
System.Diagnostics.Process.Start(pathToHelpFile);
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
private void btnMoreOptions_Click(object sender, RoutedEventArgs e)
{
m_optionsVisible = !m_optionsVisible;
Services.GetOptions().OptionsAreVisible = m_optionsVisible;
Services.GetOptions().SaveSettingsFromToolWindow();
RefreshDisplay();
}
private void cbxDefaultArgs_TextChanged(object sender, EventArgs e)
{
WriteLine(3, "cbxDefaultArgs_TextChanged: Storing new args=" + cbxDefaultArgs.Text);
Services.GetOptions().StartArgs = cbxDefaultArgs.Text;
Services.GetOptions().SaveSettingsFromToolWindow();
}
private void cbxColVisibility_SelectionChanged(object sender, EventArgs e)
{
var comboBox = sender as ComboBox;
m_colVisibility = (ColVisibility)comboBox.SelectedIndex;
Services.GetOptions().IdxGroupsAndFuncs = comboBox.SelectedIndex;
Services.GetOptions().SaveSettingsFromToolWindow();
SetColVisibility();
}
private void cbxSortOrder_SelectionChanged(object sender, EventArgs e)
{
var comboBox = sender as ComboBox;
m_sortOrder = (SortOrder)comboBox.SelectedIndex;
Services.GetOptions().IdxSortOrder = comboBox.SelectedIndex;
Services.GetOptions().SaveSettingsFromToolWindow();
RefreshTestGroupList();
RefreshTestFuncList();
}
private void cbxLogVisibility_SelectionChanged(object sender, EventArgs e)
{
var comboBox = sender as ComboBox;
m_logVisible = (comboBox.SelectedIndex == 0);
Services.GetOptions().LogIsVisible = m_logVisible;
Services.GetOptions().SaveSettingsFromToolWindow();
RefreshDisplay();
}
private void cbxMaxExecutionTime_SelectionChanged(object sender, EventArgs e)
{
var comboBox = sender as ComboBox;
m_waitMode = (WaitMode)comboBox.SelectedIndex;
Services.GetOptions().IdxMaxWaitTime = comboBox.SelectedIndex;
Services.GetOptions().SaveSettingsFromToolWindow();
}
private void cbxMemLeakCheck_SelectionChanged(object sender, EventArgs e)
{
var comboBox = sender as ComboBox;
bool leakCheck = (comboBox.SelectedIndex == 0);
Services.GetOptions().CheckForMemoryLeaks = leakCheck;
Services.GetOptions().SaveSettingsFromToolWindow();
MyGlobals.myExecutor.SetMemLeakCheck(leakCheck);
}
private void cbxOutputLevel_SelectionChanged(object sender, EventArgs e)
{
var comboBox = sender as ComboBox;
Services.GetOptions().IdxOutputLevel = comboBox.SelectedIndex;
Services.GetOptions().SaveSettingsFromToolWindow();
MyGlobals.myMainEvents.OnSetOutputLevel(comboBox.SelectedIndex + 1);
}
private void lstTestGroups_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (lstTestGroups.SelectedItem == null)
return;
txtStartArgTestGroup.Text = "";
m_selectedTestGroups.Clear();
if (m_projectInfo.appType == AppType.BOOST) // --run_test=suite1 --run_test=suite2 ...
{
foreach (MyListItem listItem in lstTestGroups.SelectedItems)
{
txtStartArgTestGroup.Text += m_testGroups[listItem.idx].GetCmdString() + " ";
m_selectedTestGroups.Add(m_testGroups[listItem.idx].GetTestGroupHierarchyString());
}
}
else // assume TTB, -selectTestFile file1,file2,file3
{
txtStartArgTestGroup.Text = "-selectTestFile ";
string sep = "";
foreach (MyListItem listItem in lstTestGroups.SelectedItems)
{
txtStartArgTestGroup.Text += sep + m_testGroups[listItem.idx].GetTestGroupHierarchyString();
m_selectedTestGroups.Add(m_testGroups[listItem.idx].GetTestGroupHierarchyString());
sep = ",";
}
}
RefreshTestFuncList();
}
private void btnRunSelectedTestGroup_Click(object sender, RoutedEventArgs e)
{
try
{
if (m_useDebugger)
{
MyGlobals.myMainEvents.OnStartDebugging(txtStartArgTestGroup.Text + " " + cbxDefaultArgs.Text);
}
else // regular execution
{
if (m_state[(int)RunMode.TEST_GROUPS] == State.RUNNING)
{
StopExecution();
}
else
{
InitForTestExecution(RunMode.TEST_GROUPS, 1);
RefreshState();
if (Services.GetOptions().BuildSolutionBeforeStartingTest)
{
MyGlobals.myMainEvents.OnStartBuilding();
}
else
{
StartProcess(m_projectInfo.GetExePath(), txtStartArgTestGroup.Text + " " + cbxDefaultArgs.Text, m_projectInfo.targetDirPath);
}
}
}
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
MyGlobals.myMainEvents.OnTestTerminated(false, m_projectInfo.GetExePath(), false, new TimeSpan(0));
}
}
private void btnRunAllTestGroups_Click(object sender, RoutedEventArgs e)
{
try
{
if (m_state[(int)RunMode.TEST_GROUPS] == State.RUNNING)
{
StopExecution();
}
else
{
if (lstTestGroups.Items.Count > 0)
{
InitForTestExecution(RunMode.TEST_GROUPS, lstTestGroups.Items.Count);
m_idxRunAllTestGroups = 0;
if (Services.GetOptions().BuildSolutionBeforeStartingTest)
{
MyGlobals.myMainEvents.OnStartBuilding();
}
else
{
StartTestGroup(0);
}
}
}
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
private void btnGoToSrcTestGroup_Click(object sender, RoutedEventArgs e)
{
try
{
if (lstTestGroups.SelectedItem == null)
{
WriteLine(1, "No test group selected!");
}
else
{
WriteLine(3, "GoTo src: selIndex=" + lstTestGroups.SelectedIndex);
MyGlobals.myMainEvents.OnOpenSourceFile(m_testGroups[lstTestGroups.SelectedIndex].fileFullPath,
m_testGroups[lstTestGroups.SelectedIndex].lineNum);
}
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
private void lstTestFuncs_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (lstTestFuncs.SelectedItem == null)
return;
txtStartArgTestFunc.Text = "";
if (m_projectInfo.appType == AppType.BOOST) // --run_test=test1 --run_test=test2 ...
{
foreach (MyListItem listItem in lstTestFuncs.SelectedItems)
{
txtStartArgTestFunc.Text += m_testFuncs[listItem.idx].GetCmdString() + " ";
}
}
else // assume TTB, -selectTestFunc func1,func2,func3
{
txtStartArgTestFunc.Text = "-selectTestFunc ";
string sep = "";
foreach (MyListItem listItem in lstTestFuncs.SelectedItems)
{
txtStartArgTestFunc.Text += sep + m_testFuncs[listItem.idx].testFunc;
sep = ",";
}
}
}
private void chkTestFuncsForSelTestGroup_CheckBoxChanged(object sender, RoutedEventArgs e)
{
try
{
RefreshTestFuncList();
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
private void btnRunSelectedTestFunc_Click(object sender, RoutedEventArgs e)
{
try
{
if (m_useDebugger)
{
MyGlobals.myMainEvents.OnStartDebugging(txtStartArgTestFunc.Text + " " + cbxDefaultArgs.Text);
}
else // regular execution
{
if (m_state[(int)RunMode.TEST_FUNCS] == State.RUNNING)
{
StopExecution();
}
else
{
InitForTestExecution(RunMode.TEST_FUNCS, 1);
RefreshState();
if (Services.GetOptions().BuildSolutionBeforeStartingTest)
{
MyGlobals.myMainEvents.OnStartBuilding();
}
else
{
StartProcess(m_projectInfo.GetExePath(), txtStartArgTestFunc.Text + " " + cbxDefaultArgs.Text, m_projectInfo.targetDirPath);
}
}
}
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
MyGlobals.myMainEvents.OnTestTerminated(false, m_projectInfo.GetExePath(), false, new TimeSpan(0));
}
}
private void btnRunAllTestFuncs_Click(object sender, RoutedEventArgs e)
{
try
{
if (m_state[(int)RunMode.TEST_FUNCS] == State.RUNNING)
{
StopExecution();
}
else
{
if (lstTestFuncs.Items.Count > 0)
{
InitForTestExecution(RunMode.TEST_FUNCS, lstTestFuncs.Items.Count);
m_idxRunAllTestFuncs = 0;
if (Services.GetOptions().BuildSolutionBeforeStartingTest)
{
MyGlobals.myMainEvents.OnStartBuilding();
}
else
{
StartTestFunc(0);
}
}
}
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
private void btnGoToSrcTestFunc_Click(object sender, RoutedEventArgs e)
{
try
{
if (lstTestFuncs.SelectedItem == null)
{
WriteLine(1, "No test func selected!");
}
else
{
WriteLine(3, "GoTo src: selIndex=" + lstTestFuncs.SelectedIndex);
MyGlobals.myMainEvents.OnOpenSourceFile(m_testFuncs[lstTestFuncs.SelectedIndex].fileFullPath,
m_testFuncs[lstTestFuncs.SelectedIndex].lineNum);
}
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
private void cbxDescTestApps_SelectionChanged(object sender, EventArgs e)
{
if (cbxDescTestApps.SelectedIndex >= 0)
{
m_idxCurrentTestApps = cbxDescTestApps.SelectedIndex;
Services.GetOptions().IdxCurrentTestApps = m_idxCurrentTestApps;
Services.GetOptions().SaveSettingsFromToolWindow();
m_state[(int)RunMode.TEST_APPS] = State.IDLE;
LoadCurrentTestApps();
RefreshState();
}
}
private void btnRunAllTestApps_Click(object sender, RoutedEventArgs e)
{
WriteLine(3, "Run all test apps");
string txt = txtTestApps.Text;
string[] tmpTestApps = txt.Split(new Char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
m_currentTestApps.Clear();
foreach (var app in tmpTestApps)
{
app.Trim();
if (app.IndexOf("//") != 0) // skip comment lines
{
m_currentTestApps.Add(app);
}
}
try
{
if (m_state[(int)RunMode.TEST_APPS] == State.RUNNING)
{
StopExecution();
}
else
{
if (m_currentTestApps.Count > 0)
{
InitForTestExecution(RunMode.TEST_APPS, m_currentTestApps.Count);
m_idxRunAllTestApps = 0;
StartTestApp(0);
}
}
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
private void btnSaveTestApps_Click(object sender, RoutedEventArgs e)
{
WriteLine(3, "Save test apps");
SaveCurrentTestApps();
}
private void btnReloadTestApps_Click(object sender, RoutedEventArgs e)
{
WriteLine(3, "Reload test apps");
LoadCurrentTestApps();
}
private void btnClearTestApps_Click(object sender, RoutedEventArgs e)
{
txtTestApps.Text = "";
}
private void cbxExportImportTestApps_SelectionChangeCommitted(object sender, EventArgs e)
{
try
{
switch (cbxExportImportTestApps.SelectedIndex)
{
case (int)ExpImpMode.EXPORT_TO_FILE:
ExportTestAppsToFile();
break;
case (int)ExpImpMode.IMPPORT_FROM_FILE:
ImportTestAppsFromFile();
break;
case (int)ExpImpMode.IMPORT_FROM_SOLUTION:
ImportTestAppsFromSolution();
break;
case (int)ExpImpMode.IMPORT_TEST_GROUPS_FROM_STARTUP_PROJECT:
ImportTestAppsFromCurrentTestGroups();
break;
case (int)ExpImpMode.IMPORT_VISIBLE_TEST_FUNCS_FROM_STARTUP_PROJECT:
ImportTestAppsFromCurrentTestFuncs();
break;
default:
break;
}
cbxExportImportTestApps.SelectedIndex = 0; // reset selection to allow mulltiple triggers for same list index
// Remark: ListItem 0 is declared as invisible (="collapsed", see xaml) within list. This item is used to always show the standard text.
// The combobox is oonly used to trigger an action from the list and not to store and display a current selection!
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
private void chkShutdown_CheckBoxChanged(object sender, RoutedEventArgs e)
{
bool selectedForShutdown = (bool)chkShutdown.IsChecked;
if (selectedForShutdown)
{
WriteLine(1, "Automatic shutdown after end of test activated!");
System.Windows.Media.SolidColorBrush shutDownColor = System.Windows.Media.Brushes.Yellow;
this.Background = shutDownColor;
mainGrid.Background = shutDownColor;
tabControl.Background = shutDownColor;
}
else
{
this.Background = m_originalBrushWindow;
mainGrid.Background = m_originalBrushWindow;
tabControl.Background = m_originalBrushWindow;
}
}
private void btnClearEvents_Click(object sender, RoutedEventArgs e)
{
lstEvents.Items.Clear();
}
private void btnCopyLog_Click(object sender, RoutedEventArgs e)
{
try
{
System.Windows.Forms.Clipboard.SetText(GetExportInfo(true) + "\n" + Services.GetListBoxItemsAsText(lstEvents));
WriteLine(1, "Copied to clipboard, ready for pasting!");
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
private void btnExportLog_Click(object sender, RoutedEventArgs e)
{
try
{
System.Windows.Forms.SaveFileDialog saveFileDialog = new System.Windows.Forms.SaveFileDialog();
saveFileDialog.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*";
saveFileDialog.FileName = GetExportInfo(false);
saveFileDialog.RestoreDirectory = true;
if (saveFileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
System.IO.File.WriteAllText(saveFileDialog.FileName,
GetExportInfo(true) + "\n" + Services.GetListBoxItemsAsText(lstEvents));
}
}
catch (Exception ex)
{
WriteLine(1, "EXCEPTION: " + ex.ToString());
}
}
//----- Helper methods for Refreshing -----
private void RefreshDisplay()
{
RefreshDisplayText();
RefreshState();
SetColVisibility();
SetRowVisibility();
}
private void RefreshDisplayText()
{
switch (m_projectInfo.appType)
{
case AppType.BOOST:
txtInfoTestGroups.Text = "BOOST test suites";
txtInfoTestFuncs.Text = "BOOST test cases";
chkTestFuncsForSelTestGroup.Content = "within suite";
break;
case AppType.TTB:
txtInfoTestGroups.Text = "TTB test files";
txtInfoTestFuncs.Text = "TTB test functions";
chkTestFuncsForSelTestGroup.Content = "within file";
break;
default:
txtInfoTestGroups.Text = "--";
txtInfoTestFuncs.Text = "--";
chkTestFuncsForSelTestGroup.Content = "--";
break;
}
}
private void RefreshState(RunMode in_runMode, TextBlock tb, string info, bool setText = true)
{
switch (m_state[(int)in_runMode])
{
case State.IDLE:
tb.Background = m_originalBrushTextBlock;
tb.Foreground = System.Windows.Media.Brushes.Black;
if (setText)
{
tb.Text = info;
}
break;
case State.RUNNING:
tb.Background = System.Windows.Media.Brushes.Orange;
tb.Foreground = System.Windows.Media.Brushes.Black;
if (setText)
{
tb.Text = "Running" + GetInfoAboutCurrentProgress(in_runMode);
}
break;
case State.SUCCEEDED:
tb.Background = System.Windows.Media.Brushes.LightGreen;
tb.Foreground = System.Windows.Media.Brushes.Black;
if (setText)
{
tb.Text = "OK" + GetInfoAboutSucceededTest(in_runMode); ;
}
break;
case State.FAILED:
tb.Background = System.Windows.Media.Brushes.Red;
tb.Foreground = System.Windows.Media.Brushes.White;
if (setText)
{
tb.Text = "FAILED" + GetInfoAboutFailedTest(in_runMode);
}
break;
}
}
private void RefreshState()
{
RefreshState(RunMode.TEST_GROUPS, txtInfoStarArgTestGroup, GetInfoStartArgTestGroup());
RefreshState(RunMode.TEST_FUNCS, txtInfoStarArgTestFunc, GetInfoStartArgTestFunc());
RefreshState(RunMode.TEST_STARTUP_PROJECT, txtInfo, "", false);
RefreshState(RunMode.TEST_APPS, txtInfoTestApps, "");
RefreshButtons();
return;
}
private void RefreshButtons()
{
if (m_useDebugger)
{
btnRunTestAll.IsEnabled = true;
btnRunSelectedTestGroup.IsEnabled = true;
btnRunSelectedTestFunc.IsEnabled = true;
btnRunAllTestGroups.IsEnabled = false;
btnRunAllTestFuncs.IsEnabled = false;
btnRunAllTestApps.IsEnabled = false;
btnRunTestAll.Content = "Debug";
btnRunSelectedTestGroup.Content = "Debug";
btnRunSelectedTestFunc.Content = "Debug";
btnRunAllTestGroups.Content = "Run each";
btnRunAllTestFuncs.Content = "Run each";
return;
}
bool running = (m_state[(int)RunMode.TEST_STARTUP_PROJECT] == State.RUNNING)
|| (m_state[(int)RunMode.TEST_GROUPS] == State.RUNNING)
|| (m_state[(int)RunMode.TEST_FUNCS] == State.RUNNING)
|| (m_state[(int)RunMode.TEST_APPS] == State.RUNNING);
chkUseDebugger.IsEnabled = !running;
btnRunTestAll.IsEnabled = !running || (m_state[(int)RunMode.TEST_STARTUP_PROJECT] == State.RUNNING);
btnRunSelectedTestGroup.IsEnabled = !running || ((m_state[(int)RunMode.TEST_GROUPS] == State.RUNNING) && (m_idxRunAllTestGroups == -1));
btnRunAllTestGroups.IsEnabled = !running || ((m_state[(int)RunMode.TEST_GROUPS] == State.RUNNING) && (m_idxRunAllTestGroups >= 0));
btnRunSelectedTestFunc.IsEnabled = !running || ((m_state[(int)RunMode.TEST_FUNCS] == State.RUNNING) && (m_idxRunAllTestFuncs == -1));
btnRunAllTestFuncs.IsEnabled = !running || ((m_state[(int)RunMode.TEST_FUNCS] == State.RUNNING) && (m_idxRunAllTestFuncs >= 0));
btnOpenProtocolFile.IsEnabled = !running;
btnRefreshAll.IsEnabled = !running;
btnRunTestAll.Content = ((btnRunTestAll.IsEnabled && running) ? "Stop" : Services.GetOptions().RunUntilFailure ? "Run until failure" : "Run");
btnRunSelectedTestGroup.Content = ((btnRunSelectedTestGroup.IsEnabled && running) ? "Stop test" : "Run selected");
btnRunAllTestGroups.Content = ((btnRunAllTestGroups.IsEnabled && running) ? "Stop test" : "Run each");
btnRunSelectedTestFunc.Content = ((btnRunSelectedTestFunc.IsEnabled && running) ? "Stop test" : "Run selected");
btnRunAllTestFuncs.Content = ((btnRunAllTestFuncs.IsEnabled && running) ? "Stop test" : "Run each");
btnRunAllTestApps.IsEnabled = !running || (m_state[(int)RunMode.TEST_APPS] == State.RUNNING);
btnRunAllTestApps.Content = ((btnRunAllTestApps.IsEnabled && running) ? "Stop" : "Run all");
btnMoreOptions.Content = (m_optionsVisible ? "Hide" : "...");
}
private void RefreshTestFuncList()
{
lstTestFuncs.Items.Clear();
bool showOnlyFuncsWithinGroup = (bool)chkTestFuncsForSelTestGroup.IsChecked;
if (showOnlyFuncsWithinGroup)
{
if (lstTestGroups.SelectedItem == null)
{
if (lstTestGroups.Items.Count == 0)
{
// no test groups available
return;
}
if (lstTestGroups.Items.Count > 0)
{
lstTestGroups.SelectedIndex = 0;
if (lstTestGroups.SelectedItem == null)
{
// nothing selected
return;
}
}
}
}
// Create local collection of test funcs which may be sorted
m_testFuncs = new System.Collections.Generic.List<TestFuncEntry>();
foreach (var testFuncEntry in m_projectInfo.testFuncs)
{
if (showOnlyFuncsWithinGroup)
{
if (m_selectedTestGroups.Contains(testFuncEntry.GetTestGroupHierarchyString()))
{
m_testFuncs.Add(testFuncEntry);
}
}
else // show all test funcs
{
m_testFuncs.Add(testFuncEntry);
}
}
switch (m_sortOrder)
{
case SortOrder.AS_READ_FROM_FILE:
// do not change sort order
break;
case SortOrder.REVERSE_READ:
{
m_testFuncs.Reverse();
}
break;
case SortOrder.ALPHA:
{
m_testFuncs.Sort((x, y) => x.GetDisplayString().CompareTo(y.GetDisplayString()));
}
break;
case SortOrder.REVERSE_ALPHA:
{
m_testFuncs.Sort((x, y) => y.GetDisplayString().CompareTo(x.GetDisplayString()));
}
break;
}
int idx = 0;
foreach (var testFuncEntry in m_testFuncs)
{
MyListItem entry = new MyListItem(idx, testFuncEntry.GetDisplayString());
lstTestFuncs.Items.Add(entry);
++idx;
}
}
private void RefreshTestGroupList()
{
int lastSelectedIndex = lstTestGroups.SelectedIndex;
lstTestGroups.Items.Clear();
// Copy test groups to local collection which may be sorted
m_testGroups = new System.Collections.Generic.List<TestGroupEntry>(m_projectInfo.testGroups);
m_selectedTestGroups = new HashSet<string>();
switch (m_sortOrder)
{
case SortOrder.AS_READ_FROM_FILE:
// do not change sort order
break;
case SortOrder.REVERSE_READ:
{
m_testGroups.Reverse();
}
break;
case SortOrder.ALPHA:
{
m_testGroups.Sort((x, y) => x.GetDisplayString().CompareTo(y.GetDisplayString()));
}
break;
case SortOrder.REVERSE_ALPHA:
{
m_testGroups.Sort((x, y) => y.GetDisplayString().CompareTo(x.GetDisplayString()));
}
break;
}
// Copy sorted entries to listbox
int idx = 0;
foreach (var testGroupEntry in m_testGroups)
{
MyListItem entry = new MyListItem(idx, testGroupEntry.GetDisplayString());
lstTestGroups.Items.Add(entry);
++idx;
}
if ((lstTestGroups.Items.Count > 0) && (lastSelectedIndex >= 0))
{
int selIndex = (lastSelectedIndex < lstTestGroups.Items.Count) ? lastSelectedIndex : lstTestGroups.Items.Count - 1;
{
lstTestGroups.SelectedIndex = selIndex;
m_selectedTestGroups.Clear();
if (selIndex >= 0)
{
m_selectedTestGroups.Add(m_testGroups[selIndex].GetTestGroupHierarchyString());
}
}
}
}
private void SetRowVisibility()
{
if (m_optionsVisible)
{
mainRowOptions.Height = new GridLength(56);
mainRowDefaultArgs.Height = new GridLength(30);
}
else
{
mainRowOptions.Height = new GridLength(0);
mainRowDefaultArgs.Height = new GridLength(0);
}
if (m_logVisible)
{
mainRowLog.Height = new GridLength(5, GridUnitType.Star);
mainRowLogButton.Height = new GridLength(30);
}
else
{
mainRowLog.Height = new GridLength(0);
mainRowLogButton.Height = new GridLength(0);
}
}
private void SetColVisibility()
{
switch (m_colVisibility)
{
case ColVisibility.BOTH:
mainColGroups.Width = new GridLength(1, GridUnitType.Star);
mainColFuncs.Width = new GridLength(1, GridUnitType.Star);
break;
case ColVisibility.LEFT:
mainColGroups.Width = new GridLength(1, GridUnitType.Star);
mainColFuncs.Width = new GridLength(0);
break;
case ColVisibility.RIGHT:
mainColGroups.Width = new GridLength(0);
mainColFuncs.Width = new GridLength(1, GridUnitType.Star);
break;
}
}
private void TriggerDelayedRefreshFromGuiThread()
{
if (m_refreshTimerIsActive)
{
WriteLine(2, "TriggerDelayedRefreshFromGuiThread: timer already active,call is ignored");
return;
}
m_refreshTimerIsActive = true;
m_refreshTimer.Tick += new EventHandler(RefreshTimer_Elapsed);
m_refreshTimer.Interval = new TimeSpan(0, 0, 1);
m_refreshTimer.Start();
WriteLine(2, "TriggerDelayedRefreshFromGuiThread: new timer started");
}
private void RefreshTimer_Elapsed(object sender, EventArgs e)
{
if (!m_refreshTimerIsActive)
{
WriteLine(2, "RefreshTimer_Elapsed: timer already stopped, ignore timer event!");
return;
}
WriteLine(2, "RefreshTimer_Elapsed: timer will be stopped, refresh is executed");
m_refreshTimerIsActive = false;
m_refreshTimer.Stop();
if (MyGlobals.myMainEvents != null)
MyGlobals.myMainEvents.OnRefreshNow();
}
//----- Helper methods to get data for display -----
private string GetInfoStartArgTestGroup()
{
switch (m_projectInfo.appType)
{
case AppType.BOOST:
return "Select test suite / edit cmd";
case AppType.TTB:
return "Select test file / edit cmd";
default:
return "--";
}
}
private string GetInfoStartArgTestFunc()
{
switch (m_projectInfo.appType)
{
case AppType.BOOST:
return "Select single test case / edit cmd";
case AppType.TTB:
return "Select single test func / edit cmd";
default:
return "--";
}
}
private string GetInfoAboutCurrentProgress(RunMode in_runMode)
{
int idx = (int)in_runMode;
if (m_state[idx] == State.RUNNING)
{
if ((in_runMode == RunMode.TEST_APPS) && (m_idxRunAllTestApps >= 0))
{
return " " + System.IO.Path.GetFileNameWithoutExtension(m_currentTestApps[m_idxRunAllTestApps])
+ " (" + (m_numExecutedTests[idx] + 1) + " of " + m_totalNumTestsToExecute[idx] + ")";
}
else
{
return " (" + (m_numExecutedTests[idx] + 1) + " of " + m_totalNumTestsToExecute[idx] + ")";
}
}
return "";
}
private string GetInfoAboutSucceededTest(RunMode in_runMode)
{
int idx = (int)in_runMode;
if (m_totalNumTestsToExecute[idx] > 1)
{
return " (" + (m_numExecutedTests[idx]) + " of " + m_totalNumTestsToExecute[idx] + ")";
}
return "";
}
private string GetInfoAboutFailedTest(RunMode in_runMode)
{
int idx = (int)in_runMode;
if (m_totalNumTestsToExecute[idx] > 1)
{
return " (" + (m_numFailedTests[idx]) + " of " + m_numExecutedTests[idx] + ")";
}
return "";
}
private string GetInfoAboutCurrentTest()
{
if ((m_curRunMode == RunMode.TEST_APPS) && (m_idxRunAllTestApps >= 0))
{
return System.IO.Path.GetFileNameWithoutExtension(m_currentTestApps[m_idxRunAllTestApps]);
}
return "";
}
private string GetExportInfo(bool in_withTimeStamp)
{
string info = "Exported from TestExecWin - " + txtInfo.Text;
if (in_withTimeStamp)
{
var now = DateTime.Now;
info += " - " + now.ToShortDateString() + " " + now.ToLongTimeString();
info = "// " + info + "\n// Source path: " + m_projectInfo.sourceDirPath + "\n";
}
return info;
}
//----- Helper methods to perform some action -----
private void InitForTestExecution(RunMode in_runMode, int in_numTestsToExecute)
{
m_stoppedByUser = false;
m_idxRunAllTestGroups = -1;
m_idxRunAllTestFuncs = -1;
m_idxRunAllTestApps = -1;
m_curRunMode = in_runMode;
int idx = (int)m_curRunMode;
m_numExecutedTests[idx] = 0;
m_numFailedTests[idx] = 0;
m_totalNumTestsToExecute[idx] = in_numTestsToExecute;
m_state[idx] = State.RUNNING;
}
private void StartProcess(string exePath, string args, string workDir)
{
int waitTimeMs = GetMaxWaitTimeInMs();
if (waitTimeMs > 0)
{
m_executionTimer.Interval = waitTimeMs;
m_executionTimer.Start();
}
MyGlobals.myMainEvents.OnStartTest(exePath, args, workDir);
}
private void StopExecution()
{
WriteLine(2, "Stopping execution...");
m_stoppedByUser = true;
MyGlobals.myMainEvents.OnStopAll();
}
private void StartTestGroup(int in_idx)
{
if (m_testGroups[in_idx].GetDisplayString() == "<default>")
{
// Simulate successful execution
TestTerminatedWithinGuiThread(true, "", "<execution skipped>", false, new TimeSpan(0));
return;
}
m_state[(int)m_curRunMode] = State.RUNNING;
RefreshState();
StartProcess(m_projectInfo.GetExePath(), m_testGroups[in_idx].GetCmdString() + " " + cbxDefaultArgs.Text, m_projectInfo.targetDirPath);
}
private void StartTestFunc(int in_idx)
{
m_state[(int)m_curRunMode] = State.RUNNING;
RefreshState();
StartProcess(m_projectInfo.GetExePath(), m_testFuncs[in_idx].GetCmdString() + " " + cbxDefaultArgs.Text, m_projectInfo.targetDirPath);
}
private void StartTestApp(int in_idx)
{
string exePathWithOptionalArgs = m_currentTestApps[in_idx];
string exeToStart = exePathWithOptionalArgs;
string args = cbxDefaultArgs.Text;
// Check for optional cmd args after executable
string tmpExeToStart = exeToStart.ToLower();
int posStartExtension = tmpExeToStart.LastIndexOf(".exe");
if (posStartExtension >= 0)
{
int posStartArgs = posStartExtension + 5;
if (posStartArgs < exeToStart.Length)
{
string specificArgs = exePathWithOptionalArgs.Substring(posStartArgs);
if (specificArgs.Length > 0)
{
args = specificArgs + " " + args;
exeToStart = exePathWithOptionalArgs.Substring(0, posStartArgs - 1);
}
}
}
m_state[(int)m_curRunMode] = State.RUNNING;
RefreshState();
StartProcess(exeToStart, args, System.IO.Path.GetDirectoryName(exeToStart));
}
private void ExportTestAppsToFile()
{
System.Windows.Forms.SaveFileDialog saveFileDialog = new System.Windows.Forms.SaveFileDialog();
saveFileDialog.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*";
saveFileDialog.FileName = cbxDescTestApps.Text;
saveFileDialog.RestoreDirectory = true;
if (saveFileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
System.IO.File.WriteAllText(saveFileDialog.FileName, txtTestApps.Text);
}
}
private void ImportTestAppsFromFile()
{
System.Windows.Forms.OpenFileDialog openFileDialog = new System.Windows.Forms.OpenFileDialog();
openFileDialog.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*";
openFileDialog.RestoreDirectory = true;
if (openFileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
string readText = System.IO.File.ReadAllText(openFileDialog.FileName);
txtTestApps.Text += "\n// Imported from file " + openFileDialog.FileName + "\n";
txtTestApps.Text += readText;
}
}
private void ImportTestAppsFromSolution()
{
txtTestApps.Text += "\n// Imported executables from current solution " + System.IO.Path.GetFileName(m_projectInfo.solutionFullPath) + "\n";
string foundExePaths = MyGlobals.myMainEvents.OnGetExecutablesFromCurrentSolution();
txtTestApps.Text += foundExePaths;
}
private void ImportTestAppsFromCurrentTestGroups()
{
string executablesWithStartArgs = "\n// Imported test suites from " + m_projectInfo.project + "\n";
for (int i = 0; i < m_testGroups.Count; ++i)
{
executablesWithStartArgs += m_projectInfo.GetExePath() + " " + m_testGroups[i].GetCmdString() + "\n";
}
txtTestApps.Text += executablesWithStartArgs;
}
private void ImportTestAppsFromCurrentTestFuncs()
{
string executablesWithStartArgs = "\n// Imported visible test cases from " + m_projectInfo.project + "\n";
for (int i = 0; i < m_testFuncs.Count; ++i)
{
executablesWithStartArgs += m_projectInfo.GetExePath() + " " + m_testFuncs[i].GetCmdString() + "\n";
}
txtTestApps.Text += executablesWithStartArgs;
}
private void LoadCurrentTestApps()
{
txtTestApps.Text = Services.GetOptions().GetTestAppsForIdx(m_idxCurrentTestApps);
}
private void SaveCurrentTestApps()
{
Services.GetOptions().SaveTestAppsForIdx(m_idxCurrentTestApps, cbxDescTestApps.Text, txtTestApps.Text);
m_dataDescTestApps[m_idxCurrentTestApps] = cbxDescTestApps.Text;
cbxDescTestApps.ItemsSource = null;
cbxDescTestApps.ItemsSource = m_dataDescTestApps;
cbxDescTestApps.SelectedIndex = m_idxCurrentTestApps;
}
private void SaveLog(RunMode in_runMode)
{
try
{
string fileName = "TextExecWin.";
if (in_runMode == RunMode.TEST_APPS)
{
fileName += "RunTestApps." + cbxDescTestApps.Text;
}
else if (in_runMode == RunMode.TEST_GROUPS)
{
fileName += "RunAllTestGroups." + txtInfo.Text;
}
else if (in_runMode == RunMode.TEST_FUNCS)
{
fileName += "RunAllTestFuncs." + txtInfo.Text;
}
else if (in_runMode == RunMode.TEST_STARTUP_PROJECT)
{
fileName += "SingleRun." + txtInfo.Text;
}
var now = DateTime.Now;
string timeStamp = now.ToString("yyyy.MM.dd_HH.mm.ss");
fileName += "." + timeStamp + ".Log.txt";
WriteLine(2, "SaveLog-fileName=" + fileName);
string fileDir = m_projectInfo.targetDirPath;
if (fileDir == "<not set>")
{
// Get the first path found within environment variables TMP, TEMP or USERPROFILE
fileDir = System.IO.Path.GetTempPath();
}
string fullFilePath = fileDir + "\\" + fileName;
WriteLine(2, "SaveLog-fullFilePath=" + fullFilePath);
string fileHeader = "// Exported from TestExecWin - " + now.ToShortDateString() + " " + now.ToLongTimeString();
fileHeader += "\n// Test project : " + txtInfo.Text;
fileHeader += "\n// Source path : " + m_projectInfo.sourceDirPath;
fileHeader += "\n// RunMode : " + in_runMode + "\n\n";
WriteLine(3, "SaveLog-fileHeader=\n" + fileHeader);
// Write list contents to file
System.IO.File.WriteAllText(fullFilePath, fileHeader + Services.GetListBoxItemsAsText(lstEvents));
}
catch (Exception ex)
{
WriteLine(1, "SaveLog-EXCEPTION: " + ex.ToString());
}
}
public void TestTerminatedWithinGuiThread(bool in_success, string infoAboutTest, string in_args, bool in_memLeaksDetected, TimeSpan in_executionTime)
{
m_executionTimer.Stop();
// Display execution status
string info = in_args;
if (info == "")
{
info = "<no args set>";
}
string durationStr;
if (in_executionTime.TotalSeconds < 1.0)
{
durationStr = "<1s ";
}
else
{
double totalNumMinutes = in_executionTime.TotalSeconds / 60.0;
durationStr = totalNumMinutes.ToString("F1") + "m ";
}
//int totalNumSeconds = Convert.ToInt32(Math.Round(in_executionTime.TotalSeconds));
//int totalNumMinutes = totalNumSeconds / 60;
//int numSeconds = totalNumSeconds % 60;
//int numSecondsAsDecimalMinute = numSeconds / 6;
//string durationStr = totalNumMinutes + "," + numSecondsAsDecimalMinute + "m ";
WriteLine(1, (!in_success ? "FAILED: " : (in_memLeaksDetected ? "FAILED (MEM_LEAKS): " : "OK: "))
+ durationStr + GetInfoAboutCurrentTest() + " " + info);
bool failure = !in_success || in_memLeaksDetected;
int idx = (int)m_curRunMode;
++m_numExecutedTests[idx];
if (failure)
{
++m_numFailedTests[idx];
}
bool testSucceededUpToNow = (m_numFailedTests[idx] == 0);
// Safe update of state variables and refresh
// of GUI controls within GUI thread
SetState(testSucceededUpToNow);
/// ...
/// [dispatch test terminate msg]
if (!m_stoppedByUser)
{
if (m_idxRunAllTestGroups >= 0)
{
++m_idxRunAllTestGroups;
if (m_idxRunAllTestGroups >= lstTestGroups.Items.Count)
{
m_idxRunAllTestGroups = -1;
CheckForFinalAction(RunMode.TEST_GROUPS);
}
else // start next test
{
StartTestGroup(m_idxRunAllTestGroups);
}
}
else if (m_idxRunAllTestFuncs >= 0)
{
++m_idxRunAllTestFuncs;
if (m_idxRunAllTestFuncs >= lstTestFuncs.Items.Count)
{
m_idxRunAllTestFuncs = -1;
CheckForFinalAction(RunMode.TEST_FUNCS);
}
else // start next test
{
StartTestFunc(m_idxRunAllTestFuncs);
}
}
else if (m_idxRunAllTestApps >= 0)
{
++m_idxRunAllTestApps;
if (m_idxRunAllTestApps >= m_currentTestApps.Count)
{
// last test app has terminated
m_idxRunAllTestApps = -1;
CheckForFinalAction(RunMode.TEST_APPS);
}
else // start next test
{
StartTestApp(m_idxRunAllTestApps);
}
}
else
{
if (Services.GetOptions().RunUntilFailure && m_curRunMode == RunMode.TEST_STARTUP_PROJECT)
{
if (in_success)
{
WriteLine(1, "Execution " + m_numExecutedTests[idx].ToString() + " succeeded. Continue run cycle...");
m_state[idx] = State.RUNNING;
RefreshState();
StartProcess(m_projectInfo.GetExePath(), cbxDefaultArgs.Text, m_projectInfo.targetDirPath);
}
else
{
WriteLine(1, "Execution " + m_numExecutedTests[idx].ToString() + " failed. Exiting run cycle.");
CheckForFinalAction(RunMode.TEST_STARTUP_PROJECT);
}
}
else
{
CheckForFinalAction(RunMode.TEST_STARTUP_PROJECT);
}
}
}
}
private void OnExecutionTimeout(Object myObject, EventArgs myEventArgs)
{
m_executionTimer.Stop();
WriteLine(1, "TIMEOUT of " + GetMaxWaitTimeInMs() / 1000 + " sec has expired!");
MyGlobals.myExecutor.KillRunningProcess();
}
private void CheckForFinalAction(RunMode in_runMode)
{
try
{
bool shutdown = (bool)chkShutdown.IsChecked;
if (shutdown)
{
SaveLog(in_runMode);
bool reallyShutdown = true; // deactivate for testing
if (reallyShutdown)
{
// Possible extension: First check for existence of predefined batch file to perform specific shutdown actions
//string nameBatchFile="TestExecWin.Shutdown.cmd";
//string foundBatchFilePath = "";
//System.Collections.Generic.List<string> possibleFilePaths = new System.Collections.Generic.List<string>();
//if (m_projectInfo.targetDirPath != "<not set>")
//{
// possibleFilePaths.Add(m_projectInfo.targetDirPath);
//}
//possibleFilePaths.Add(System.IO.Path.GetTempPath());
//foreach (string fileDir in possibleFilePaths)
//{
// string tmpPath = fileDir + nameBatchFile;
// if (System.IO.File.Exists(tmpPath))
// {
// foundBatchFilePath = tmpPath;
// break;
// }
// WriteLine(1, "Batch not found: " + tmpPath);
//}
//if (foundBatchFilePath.Length>0)
//{
// WriteLine(1, "Trying to shutdown with " + foundBatchFilePath + "...");
// Process.Start(foundBatchFilePath);
//}
//else
{
// WriteLine(1, "No predefined batch file found, now trying to shutdown with simple shutdown command...");
WriteLine(1, "Now trying to shutdown with simple command shutdown /s /f ...");
Process.Start("shutdown", "/s /f /t 0");
}
}
else
{
WriteLine(1, "Shutdown is skipped");
}
}
}
catch (Exception ex)
{
WriteLine(1, "CheckForFinalAction-EXCEPTION: " + ex.ToString());
}
}
//----- General helper methods -----
private void SetState(bool in_success)
{
if (m_state[(int)m_curRunMode] == State.RUNNING)
{
m_state[(int)m_curRunMode] = (in_success ? State.SUCCEEDED : State.FAILED);
}
RefreshState();
}
public bool TestIsRunning()
{
for (int i = 0; i < 4; ++i)
{
if (m_state[i] == State.RUNNING)
return true;
}
return false;
}
private int GetMaxWaitTimeInMs()
{
switch (m_waitMode)
{
case WaitMode.WAIT_ENDLESS:
return 0;
case WaitMode.WAIT_30_SEC:
return 30 * 1000;
case WaitMode.WAIT_1_MIN:
return 60 * 1000;
case WaitMode.WAIT_2_MIN:
return 120 * 1000;
case WaitMode.WAIT_5_MIN:
return 300 * 1000;
case WaitMode.WAIT_10_MIN:
return 600 * 1000;
case WaitMode.WAIT_20_MIN:
return 1200 * 1000;
case WaitMode.WAIT_30_MIN:
return 1800 * 1000;
case WaitMode.WAIT_40_MIN:
return 2400 * 1000;
case WaitMode.WAIT_60_MIN:
return 3600 * 1000;
default:
return 3600 * 1000;
}
}
private void WriteLine(int in_outputLevel, String in_info)
{
if (MyGlobals.myEventReceiver != null)
{
MyGlobals.myEventReceiver.WriteLine(in_outputLevel, in_info);
}
}
/// [threading sync gui thread]
public void AddInfoToEventList(string info)
{
// Redirect to main GUI thread
Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.Run(async delegate {
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
AddInfoToEventListFromGuiThread(info);
});
}
private void AddInfoToEventListFromGuiThread(string info)
{
// here we are sure to be within GUI thread and we can
// safely access the list box
lstEvents.Items.Add(info);
/// ...
/// [threading sync gui thread]
// Ensure last added item is visible
ListBoxAutomationPeer svAutomation = (ListBoxAutomationPeer)ScrollViewerAutomationPeer.CreatePeerForElement(lstEvents);
IScrollProvider scrollInterface = (IScrollProvider)svAutomation.GetPattern(PatternInterface.Scroll);
System.Windows.Automation.ScrollAmount scrollVertical = System.Windows.Automation.ScrollAmount.LargeIncrement;
System.Windows.Automation.ScrollAmount scrollHorizontal = System.Windows.Automation.ScrollAmount.NoAmount;
//If the vertical scroller is not available, the operation cannot be performed, which will raise an exception.
if (scrollInterface == null)
{
return;
}
if (scrollInterface.VerticallyScrollable)
scrollInterface.Scroll(scrollHorizontal, scrollVertical);
}
}
}


TestExecWindow: MyToolWindowCommand.cs

TestExecWindow
MyToolWindowCommand.cs
Internal Design » Source files
//------------------------------------------------------------------------------
// Copyright(C) 2022 Gerald Fahrnholz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// Contact: http://www.gerald-fahrnholz.eu/impressum.php
//------------------------------------------------------------------------------
namespace TestExecWin_VS2022
{
[Command(PackageIds.MyCommand)]
internal sealed class MyToolWindowCommand : BaseCommand<MyToolWindowCommand>
{
protected override Task ExecuteAsync(OleMenuCmdEventArgs e)
{
return MyToolWindow.ShowAsync();
}
}
}


TestExecWindow: TestExecWin_VS2022Package.cs

TestExecWindow
TestExecWin_VS2022Package.cs
Internal Design » Source files
//------------------------------------------------------------------------------
// Copyright(C) 2022 Gerald Fahrnholz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// Contact: http://www.gerald-fahrnholz.eu/impressum.php
//------------------------------------------------------------------------------
global using Community.VisualStudio.Toolkit;
global using Microsoft.VisualStudio.Shell;
global using System;
global using Task = System.Threading.Tasks.Task;
using System.Runtime.InteropServices;
using System.Threading;
namespace TestExecWin_VS2022
{
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)]
[ProvideToolWindow(typeof(MyToolWindow.Pane), Style = VsDockStyle.Tabbed, Window = WindowGuids.SolutionExplorer)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid(PackageGuids.TestExecWin_VS2022String)]
[ProvideOptionPage(typeof(OptionPage), "TestExecWindow", "Options", 1, 1, true, new string[] { "TestExecWindow Options" })]
/// [InitializePackageAndDTE]
public sealed class TestExecWin_VS2022Package : ToolkitPackage
{
protected override async Task InitializeAsync(CancellationToken cancellationToken,
IProgress<ServiceProgressData> progress)
{
await this.RegisterCommandsAsync();
this.RegisterToolWindows(); // => calls MyToolWindow constructor
// Remark: In this specific project some object references are stored within the global struct
// MyGlobals for easy access from any point of code.
// Allow access to package (e.g. for getting options page which is managed by package)
MyGlobals.myPackage = this;
// Establish connection to automation object DTE of Visual Studio
if (MyGlobals.myDTE == null)
{
await JoinableTaskFactory.SwitchToMainThreadAsync();
var dte = await this.GetServiceAsync(typeof(EnvDTE.DTE)) as EnvDTE80.DTE2;
Microsoft.Assumes.Present(dte);
// Pass DTE reference to static data members of other classes
MyGlobals.myDTE = dte;
VisualStudioConnector.dte = dte;
}
}
}
/// [InitializePackageAndDTE]
}


TestExecWindow: VisualStudioConnector.cs

TestExecWindow
VisualStudioConnector.cs
Internal Design » Source files
//------------------------------------------------------------------------------
// Copyright(C) 2022 Gerald Fahrnholz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// Contact: http://www.gerald-fahrnholz.eu/impressum.php
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio;
namespace TestExecWin_VS2022
{
/// [derive from notification interfaces]
/// Class derives from notification interfaces to get
/// informed when user performs changes within Visual Studio.
class VisualStudioConnector
: Microsoft.VisualStudio.Shell.Interop.IVsSelectionEvents
, Microsoft.VisualStudio.Shell.Interop.IVsUpdateSolutionEvents
/// [derive from notification interfaces]
{
public static EnvDTE80.DTE2 dte;
private EnvDTE.Project m_startupProj = null;
private IVsMonitorSelection m_monitorSelection = null;
private uint m_selectionEventsCookie = 0;
private IVsSolutionBuildManager m_solutionBuildManager = null;
private uint m_updateSolutionEventsCookie = 0;
private IMainEvents m_mainEvents;
private IEventReceiver m_evenReceiver;
private bool m_buildStartedByMe = false;
private int m_numFailedProjects = 0;
private string m_platFormStartupProject= "";
public VisualStudioConnector(IMainEvents in_mainEvents, IEventReceiver in_eventReceiver)
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
m_mainEvents = in_mainEvents;
m_evenReceiver = in_eventReceiver;
ConnectWithVisualStudio();
}
catch (Exception ex)
{
WriteLine(1, "DisconnectFromVisualStudio: EXCEPTION: " + ex.ToString());
}
}
/// [connect with VS]
private void ConnectWithVisualStudio()
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
bool connectedToSelectionEvents = false;
bool connectedToUpdateSolutionEvents = false;
// Advise to selection events (e.g. startup project changed)
m_monitorSelection = Microsoft.VisualStudio.Shell.ServiceProvider.GlobalProvider.
GetService(typeof(SVsShellMonitorSelection)) as IVsMonitorSelection;
if (m_monitorSelection != null)
{
m_monitorSelection.AdviseSelectionEvents(
this, out m_selectionEventsCookie);
connectedToSelectionEvents = true;
}
else
{
WriteLine(1, "ConnectWithVisualStudio: GetService(SVsShellMonitorSelection) failed!");
}
// Advise to update solution events (e.g. switched debug/release configuration)
m_solutionBuildManager = Microsoft.VisualStudio.Shell.ServiceProvider.GlobalProvider.
GetService(typeof(SVsSolutionBuildManager)) as IVsSolutionBuildManager;
if (m_solutionBuildManager != null)
{
m_solutionBuildManager.AdviseUpdateSolutionEvents(
this, out m_updateSolutionEventsCookie);
connectedToUpdateSolutionEvents = true;
}
else
{
WriteLine(1, "ConnectWithVisualStudio: GetService(SVsSolutionBuildManager) failed!");
}
if (connectedToSelectionEvents && connectedToUpdateSolutionEvents)
{
WriteLine(1, "ConnectWithVisualStudio succeeded");
}
}
catch (Exception ex)
{
WriteLine(1, "ConnectWithVisualStudio failed: EXCEPTION: " + ex.ToString());
}
}
/// [connect with VS]
/// [disconnect from VS]
/// Release all connections to Visual Studio.
/// This function is called from Dispose method of main app class TestExecWindow
public void DisconnectFromVisualStudio()
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
if (m_monitorSelection != null && m_selectionEventsCookie != 0)
m_monitorSelection.UnadviseSelectionEvents(m_selectionEventsCookie);
if (m_solutionBuildManager != null && m_updateSolutionEventsCookie != 0)
m_solutionBuildManager.UnadviseUpdateSolutionEvents(m_updateSolutionEventsCookie);
}
catch (Exception ex)
{
WriteLine(1, "DisconnectFromVisualStudio: EXCEPTION: " + ex.ToString());
}
}
/// [disconnect from VS]
/// Read all relevant project settings
public bool ReadSettingsOfStartupProject(ProjectInfo projectInfo)
{
WriteLine(3, "ReadSettingsOfStartupProject-Begin");
try
{
ThreadHelper.ThrowIfNotOnUIThread();
if (dte == null)
{
WriteLine(1, "dte is null (checking for startup project is not possible)");
return false;
}
if (dte.Solution == null)
{
WriteLine(1, "dte.Solution is null (checking for startup project is not possible)");
return false;
}
if (dte.Solution.SolutionBuild == null)
{
WriteLine(1, "dte.Solution.SolutionBuild is null (checking for startup project is not possible)");
return false;
}
if (dte.Solution.SolutionBuild.ActiveConfiguration == null)
{
WriteLine(1, "dte.Solution.SolutionBuild.ActiveConfiguration is null (checking for startup project is not possible)");
return false;
}
projectInfo.solutionFullPath = dte.Solution.FileName;
/// [get config name]
//Get name of config (e.g. "Debug", "Release")
string configName = dte.Solution.SolutionBuild.ActiveConfiguration.Name;
/// [get config name]
string msg = "";
/// [get name startup project]
EnvDTE80.SolutionBuild2 sb = (EnvDTE80.SolutionBuild2)dte.Solution.SolutionBuild;
if (sb == null)
{
WriteLine(1, "SolutionBuild is null (checking for startup project is not possible)");
return false;
}
// sb.StartupProjects is an array of project file names. Usually we expect
// only a single project name within the array.
// Name is of the format "..\Test\TestRunner\TestRunner.vcxproj"
if (sb.StartupProjects == null)
{
WriteLine(2, "ReadSettingsOfStartupProject-End: StartupProjects not available");
return false;
}
foreach (String item in (Array)sb.StartupProjects)
{
msg += item;
}
WriteLine(3, "startupProjects=" + msg);
// for further processing extract the name without path and extension
// e.g. nameStartupProject = "TestRunner"
// (extraction code not shown here)
/// [get name startup project]
int posLast = msg.LastIndexOf(".");
if (posLast >= 0)
{
msg = msg.Substring(0, posLast);
}
WriteLine(3, "startupProjects=" + msg);
int posFirst = msg.LastIndexOf("\\");
if (posFirst >= 0)
{
msg = msg.Substring(posFirst + 1);
}
String nameStartupProject = msg;
WriteLine(3, "nameStartupProject=" + nameStartupProject);
// To simple solution which does not work for startup projects within a filter
//EnvDTE.Project startupProj = dte.Solution.Item(msg);
/// [get startup project]
// Perform recursive search for the startup project through all solution filters:
EnvDTE.Project startupProj = FindProject(nameStartupProject);
/// [get startup project]
if (startupProj == null)
{
WriteLine(1, "No startup project found");
WriteLine(3, "ReadSettingsOfStartupProject-End: no startup project found");
return false;
}
m_startupProj = startupProj;
projectInfo.project = startupProj.Name;
projectInfo.config = configName;
projectInfo.sourceDirPath = System.IO.Path.GetDirectoryName(startupProj.FullName);
WriteLine(2, "ReadSettingsOfStartupProject: project=" + projectInfo.project + " - " + projectInfo.config);
WriteLine(2, "ReadSettingsOfStartupProject: projectPath=" + projectInfo.sourceDirPath);
EnvDTE.ConfigurationManager cm = startupProj.ConfigurationManager;
if (cm == null)
{
WriteLine(1, "No ConfigurationManager found");
WriteLine(3, "ReadSettingsOfStartupProject-End: no ConfigurationManager found");
return false;
}
if (cm.ActiveConfiguration == null)
{
WriteLine(1, "No ActiveConfiguration found");
WriteLine(3, "ReadSettingsOfStartupProject-End: no ActiveConfiguration found");
return false;
}
m_platFormStartupProject = cm.ActiveConfiguration.PlatformName;
projectInfo.platform = m_platFormStartupProject;
WriteLine(2, "ReadSettingsOfStartupProject: platform=" + projectInfo.platform);
EnvDTE.Properties props = cm.ActiveConfiguration.Properties;
if (props != null)
{
WriteLine(2, "Now iterating over ActiveConfiguration.Properties...");
// Scan properties of ActiveConfiguration to be used for future extended requests
msg = "ReadSettingsOfStartupProject: ActiveConfiguration.Properties";
foreach (EnvDTE.Property p in props)
{
msg += " " + p.Name;
}
WriteLine(2, msg);
}
//projectInfo.targetDirPath = startupProj.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value.ToString();
//msg = "ReadSettingsOfStartupProject: OutputPath=" + projectInfo.targetDirPath;
//WriteLine(2, msg);
/// [get exe path]
// Get full path of executable depending on found
// startup project and configuration (Debug/Release)
Microsoft.VisualStudio.VCProjectEngine.VCProject vcProj = null;
try
{
vcProj = (Microsoft.VisualStudio.VCProjectEngine.VCProject)startupProj.Object;
if (vcProj ==null)
{
WriteLine(1, "Project is not of type VCProject and is not supported!");
return false;
}
}
catch (Exception ex)
{
WriteLine(2, "ReadSettingsOfStartupProject-End: EXCEPTION: " + ex.ToString());
WriteLine(1, "Project is not of type VCProject and is not supported!");
return false;
}
Microsoft.VisualStudio.VCProjectEngine.IVCCollection configs =
(Microsoft.VisualStudio.VCProjectEngine.IVCCollection)vcProj.Configurations;
Microsoft.VisualStudio.VCProjectEngine.VCConfiguration config =
FindConfig(configs, configName, m_platFormStartupProject);
if (config == null)
{
WriteLine(1, "Config " + configName + " not found");
return false;
}
msg = "PrimaryOutput (FullExePath)=" + config.PrimaryOutput;
WriteLine(2, msg);
/// [get exe path]
projectInfo.fullExePath = config.PrimaryOutput;
string delimiter = "/\\";
int posPathEnd = projectInfo.fullExePath.LastIndexOfAny(delimiter.ToCharArray());
if (posPathEnd > 0)
{
projectInfo.targetDirPath = projectInfo.fullExePath.Substring(0, posPathEnd);
}
msg = "ReadSettingsOfStartupProject: OutputPath=" + projectInfo.targetDirPath;
WriteLine(2, msg);
// Scan properties to be used for future extended requests
msg = "ReadSettingsOfStartupProject: startupProj.Properties";
foreach (EnvDTE.Property p in startupProj.Properties)
{
msg += " " + p.Name;
}
WriteLine(3, msg);
//msg = "ReadSettingsOfStartupProject: ActiveConfiguration.Properties";
//foreach (EnvDTE.Property p in startupProj.ConfigurationManager.ActiveConfiguration.Properties)
//{
// msg += " " + p.Name;
//}
//WriteLine(3, msg);
//msg = "CommandArguments=";
//msg += startupProj.ConfigurationManager.ActiveConfiguration.Properties.Item("CommandArguments").Value.ToString();
//WriteLine(3, msg);
WriteLine(3, "ReadSettingsOfStartupProject-End");
}
catch (Exception ex)
{
WriteLine(1, "ReadSettingsOfStartupProject-End: EXCEPTION: " + ex.ToString());
return false;
}
return true;
}
public void BuildProject()
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
// Verify that build is not yet running
EnvDTE.SolutionBuild sb = dte.Solution.SolutionBuild;
WriteLine(2, "BuildProject: Current BuildState is " + sb.BuildState.ToString());
if (sb.BuildState == EnvDTE.vsBuildState.vsBuildStateInProgress)
{
WriteLine(1, "A build is already in progress");
m_mainEvents.OnBuildTerminated(false);
return;
}
// Register for build notifications
dte.Events.BuildEvents.OnBuildDone -= BuildEvents_OnBuildDone;
dte.Events.BuildEvents.OnBuildDone += BuildEvents_OnBuildDone;
dte.Events.BuildEvents.OnBuildProjConfigDone -= BuildEvents_OnBuildProjConfigDone;
dte.Events.BuildEvents.OnBuildProjConfigDone += BuildEvents_OnBuildProjConfigDone;
// Start build
m_buildStartedByMe = true;
m_numFailedProjects = 0;
dte.ExecuteCommand("Build.BuildSolution");
WriteLine(2, "BuildProject started...");
}
catch (Exception ex)
{
m_buildStartedByMe = false;
WriteLine(1, "BuildProject failed\nEXCEPTION: " + ex.ToString());
m_mainEvents.OnBuildTerminated(false);
}
}
private void BuildEvents_OnBuildProjConfigDone(string project, string projectConfig, string platform, string solutionConfig, bool success)
{
WriteLine(2, "BuildEvents_OnBuildProjConfigDone: project=" + project + " projectConfig= " + projectConfig + " platform=" + platform
+ " solutionConfig=" + solutionConfig + " success=" + success);
if (!success && m_buildStartedByMe)
{
WriteLine(1, "ERROR: Build failed for project " + project);
m_numFailedProjects++;
}
}
private void BuildEvents_OnBuildDone(EnvDTE.vsBuildScope scope, EnvDTE.vsBuildAction action)
{
WriteLine(2, "BuildEvents_OnBuildDone: scope=" + scope.ToString() + " action=" + action.ToString());
if (!m_buildStartedByMe)
{
WriteLine(2, "BuildEvents_OnBuildDone: event ignored, build was not started by me!");
return;
}
m_mainEvents.OnBuildTerminated(m_numFailedProjects == 0);
m_buildStartedByMe = false;
}
public void StopBuildingProject()
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
// Verify that build is running and was started by me
EnvDTE.SolutionBuild sb = dte.Solution.SolutionBuild;
WriteLine(2, "StopBuildingProject: Current BuildState is " + sb.BuildState.ToString() + " startedByMe=" + m_buildStartedByMe.ToString());
if (sb.BuildState == EnvDTE.vsBuildState.vsBuildStateInProgress && m_buildStartedByMe)
{
WriteLine(1, "Stopping build...");
dte.ExecuteCommand("Build.Cancel");
}
else
{
WriteLine(2, "StopBuildingProject: nothing to do");
}
}
catch (Exception ex)
{
WriteLine(1, "StopBuildingProject failed\nEXCEPTION: " + ex.ToString());
}
}
/// [start debugging]
public void StartDebugging(string in_cmdLineParams)
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
if (m_startupProj != null)
{
string cmdLineParams = "";
if (in_cmdLineParams != null)
{
cmdLineParams = in_cmdLineParams;
}
//m_startupProj.ConfigurationManager.ActiveConfiguration.Properties.
// Item("CommandArguments").Value = cmdLineParams;
string configName = dte.Solution.SolutionBuild.ActiveConfiguration.Name;
Microsoft.VisualStudio.VCProjectEngine.VCProject vcProj =
(Microsoft.VisualStudio.VCProjectEngine.VCProject)m_startupProj.Object;
Microsoft.VisualStudio.VCProjectEngine.IVCCollection configs =
(Microsoft.VisualStudio.VCProjectEngine.IVCCollection)vcProj.Configurations;
Microsoft.VisualStudio.VCProjectEngine.VCConfiguration config =
FindConfig(configs, configName, m_platFormStartupProject);
Microsoft.VisualStudio.VCProjectEngine.VCDebugSettings dbgSettings =
(Microsoft.VisualStudio.VCProjectEngine.VCDebugSettings)config.DebugSettings;
dbgSettings.CommandArguments = cmdLineParams;
WriteLine(2, "StartDebugging: now starting debugger with dbgSettings.CommandArguments=" + dbgSettings.CommandArguments);
dte.Debugger.Go(false /* do not wait for end of debugging*/);
}
}
catch (Exception ex)
{
WriteLine(1, "Could not start debugging\nEXCEPTION: " + ex.ToString());
}
}
/// [start debugging]
/// [open text file]
public void OpenFile(string in_fullFilePath, int in_lineNum = 0)
{
WriteLine(3, "OpenFile " + in_fullFilePath + " line " + in_lineNum);
try
{
ThreadHelper.ThrowIfNotOnUIThread();
dte.ItemOperations.OpenFile(in_fullFilePath, EnvDTE.Constants.vsViewKindTextView);
if (in_lineNum>0)
{
dte.ExecuteCommand("Edit.Goto",in_lineNum.ToString());
}
}
catch (Exception ex)
{
WriteLine(1, "Could not open file " + in_fullFilePath + "\nEXCEPTION: " + ex.ToString());
}
}
/// [open text file]
public string GetExecutablesFromCurrentSolution()
{
WriteLine(3, "GetExecutablesFromCurrentSolution-Begin");
try
{
ThreadHelper.ThrowIfNotOnUIThread();
if (dte == null)
{
WriteLine(1, "dte is null (checking for projects is not possible)");
return "";
}
if (dte.Solution == null)
{
WriteLine(1, "dte.Solution is null (checking for projects is not possible)");
return "";
}
if (dte.Solution.SolutionBuild == null)
{
WriteLine(1, "dte.Solution.SolutionBuild is null (checking for projects is not possible)");
return "";
}
if (dte.Solution.SolutionBuild.ActiveConfiguration == null)
{
WriteLine(1, "dte.Solution.SolutionBuild.ActiveConfiguration is null (checking for projects is not possible)");
return "";
}
string configName = dte.Solution.SolutionBuild.ActiveConfiguration.Name;
var projects = dte.Solution.Projects;
if (projects==null)
{
WriteLine(2, "GetExecutablesFromCurrentSolution-End: projects not available");
return "";
}
List<string> executables = new List<string>();
foreach (EnvDTE.Project project in projects)
{
if (project != null)
{
AddProjectRecursive(project, configName, executables);
}
}
WriteLine(2, "GetExecutablesFromCurrentSolution-End numExecutablesFound=" + executables.Count);
executables.Sort();
string executablesAsString = "";
foreach (string executable in executables)
{
executablesAsString += executable + "\n";
}
return executablesAsString;
}
catch (Exception ex)
{
WriteLine(1, "GetExecutablesFromCurrentSolution-End: EXCEPTION: " + ex.ToString());
return "";
}
}
private void AddProjectRecursive(EnvDTE.Project project, string configName, List<string> foundExecutables)
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
if (project == null)
{
return;
}
WriteLine(3, "AddProjectRecursive: found project.Name= " + project.Name + " project.Kind=" + project.Kind);
if (project.Kind == EnvDTE.Constants.vsProjectKindSolutionItems)
{
WriteLine(3, "AddProjectRecursive: found solution item " + project.Name);
foreach (EnvDTE.ProjectItem item in project.ProjectItems)
{
EnvDTE.Project realProject = item.Object as EnvDTE.Project;
if (realProject != null)
{
AddProjectRecursive(realProject, configName, foundExecutables);
}
}
}
else // may be real project
{
Microsoft.VisualStudio.VCProjectEngine.VCProject vcProj = (Microsoft.VisualStudio.VCProjectEngine.VCProject)project.Object;
if (vcProj == null)
{
WriteLine(3, "AddProjectRecursive/may be real project: vcProj == null");
return;
}
Microsoft.VisualStudio.VCProjectEngine.IVCCollection configs = (Microsoft.VisualStudio.VCProjectEngine.IVCCollection)vcProj.Configurations;
Microsoft.VisualStudio.VCProjectEngine.VCConfiguration config = FindConfig(configs, configName, m_platFormStartupProject);
if (config == null)
{
WriteLine(3, "AddProjectRecursive/may be real project: config == null (configName=" + configName + " " + m_platFormStartupProject + ")");
return;
}
string outputPath = config.PrimaryOutput;
outputPath.Trim();
string tmpOutputPath = outputPath.ToLower();
if ((tmpOutputPath.Length > 0) && (tmpOutputPath.IndexOf(".exe") == tmpOutputPath.Length - 4))
{
WriteLine(3, "AddProjectRecursive: Found project: " + project.Name + " exe: " + outputPath);
foundExecutables.Add(outputPath);
}
else
{
WriteLine(3, "AddProjectRecursive: Ignored project: " + project.Name + " is no exe: " + outputPath);
}
}
}
catch (Exception ex)
{
WriteLine(1, "AddProjectRecursive: EXCEPTION: " + ex.ToString());
}
}
/// [listen to changed startup project]
// ----- Interface IVsSelectionEvents -----
// Check for notification about changed startup project
int IVsSelectionEvents.OnElementValueChanged(
uint elementid, object varValueOld, object varValueNew)
{
if (elementid == (uint)VSConstants.VSSELELEMID.SEID_StartupProject)
{
// When startup project is set in solution explorer a complete refresh is triggered
if (varValueNew != null)
{
WriteLine(2, "Detected new StartupProject");
m_mainEvents.OnRefreshAll();
}
}
return VSConstants.S_OK;
}
// All other events from IVsSelectionEvents are ignored.
// Eventhandler functions simply return OK status.
int IVsSelectionEvents.OnCmdUIContextChanged(
uint dwCmdUICookie, int fActive)
{
return VSConstants.S_OK;
}
/// ...
/// [listen to changed startup project]
int IVsSelectionEvents.OnSelectionChanged(IVsHierarchy pHierOld, uint itemidOld, IVsMultiItemSelect pMISOld, ISelectionContainer pSCOld, IVsHierarchy pHierNew, uint itemidNew, IVsMultiItemSelect pMISNew, ISelectionContainer pSCNew)
{
return VSConstants.S_OK;
}
/// [listen to switched debug/release configuration]
// ----- Interface IVsUpdateSolutionEvents -----
int IVsUpdateSolutionEvents.OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
{
WriteLine(2, "Detected OnActiveProjectCfgChange");
m_mainEvents.OnRefreshAll();
return VSConstants.S_OK;
}
/// [listen to switched debug/release configuration]
// All other events from IVsSelectionEvents are ignored.
// Eventhandler functions simply return OK status.
int IVsUpdateSolutionEvents.UpdateSolution_Begin(ref int pfCancelUpdate)
{
return VSConstants.S_OK;
}
int IVsUpdateSolutionEvents.UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand)
{
return VSConstants.S_OK;
}
int IVsUpdateSolutionEvents.UpdateSolution_StartUpdate(ref int pfCancelUpdate)
{
return VSConstants.S_OK;
}
int IVsUpdateSolutionEvents.UpdateSolution_Cancel()
{
return VSConstants.S_OK;
}
// ----- Private methods -----
/// Find a project with a given name within the hierarchical tree of project/solution items
private EnvDTE.Project FindProject(string nameProject)
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
/// [iterate dte projects]
foreach (EnvDTE.Project project in dte.Solution.Projects)
/// [iterate dte projects]
{
if (project != null)
{
WriteLine(3, "FindProject: " + project.Name);
if (project.Kind == EnvDTE.Constants.vsProjectKindSolutionItems)
{
EnvDTE.Project projectNextLevel = FindProjectRecursive(project, nameProject);
if (projectNextLevel != null)
{
return projectNextLevel;
}
}
else if (project.Name == nameProject)
{
return project;
}
}
}
return null;
}
catch (Exception ex)
{
WriteLine(1, "FindProject: EXCEPTION: " + ex.ToString());
return null;
}
}
/// Find a project with a given name within the project items of a given project
private EnvDTE.Project FindProjectRecursive(EnvDTE.Project project, string nameProject)
{
try
{
ThreadHelper.ThrowIfNotOnUIThread();
if (project == null)
{
return null;
}
WriteLine(3, "FindProjectRecursive: " + project.Name);
/// [recursive search]
foreach (EnvDTE.ProjectItem item in project.ProjectItems)
{
EnvDTE.Project realProject = item.Object as EnvDTE.Project;
if (realProject != null)
{
if (realProject.Kind == EnvDTE.Constants.vsProjectKindSolutionItems)
{
EnvDTE.Project projectNextLevel = FindProjectRecursive(
realProject, nameProject);
if (projectNextLevel != null)
{
return projectNextLevel;
}
}
else if (realProject.Name == nameProject)
{
return realProject;
}
}
}
/// [recursive search]
return null;
}
catch (Exception ex)
{
WriteLine(1, "FindProjectRecursive: EXCEPTION: " + ex.ToString());
return null;
}
}
private void WriteLine(int in_outputLevel, String in_info)
{
m_evenReceiver.WriteLine(in_outputLevel, in_info);
}
/// [find config]
private Microsoft.VisualStudio.VCProjectEngine.VCConfiguration FindConfig(Microsoft.VisualStudio.VCProjectEngine.IVCCollection in_configurations, String in_configName, String in_platform)
{
string expectedConfigName = in_configName + "|" + in_platform;
WriteLine(3, "FindConfig: searching for config " + expectedConfigName);
if ((in_configurations == null) || (in_configurations.Count <= 0))
{
WriteLine(3, "FindConfig: in_configurations is null or empty");
return null;
}
WriteLine(3, "FindConfig: configurations.Count=" + in_configurations.Count);
foreach (Microsoft.VisualStudio.VCProjectEngine.VCConfiguration configItem in in_configurations)
{
WriteLine(3, "FindConfig: configItem.Name=>" + configItem.Name + "< .ConfigurationName=>" + configItem.ConfigurationName);
if (configItem.Name == expectedConfigName)
{
return configItem;
}
}
return null;
}
/// [find config]
}
}


TestExecWindow: Hidden page files of documentation

TestExecWindow
Hidden page files of documentation
Internal Design » Source files

Installation

Author
Gerald Fahrnholz

Insert picture at this hidden location to force Doxygen to copy the image file to the generated documentation. Image will be used within html img tag on main page:

UserGuide

Author
Gerald Fahrnholz

VisualStudioExtension

Author
Gerald Fahrnholz

Package

Author
Gerald Fahrnholz

VisualStudioConnector

Author
Gerald Fahrnholz

SourceFileParser

Author
Gerald Fahrnholz

Executor

Author
Gerald Fahrnholz

CommonFunctionality

Author
Gerald Fahrnholz

MultiThreading

Author
Gerald Fahrnholz

InternalDesign

Author
Gerald Fahrnholz