├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── SnapshotTesting ├── Matcher.qml ├── MatcherContentView.qml ├── ScaleToFitImage.qml ├── ScreenshotBrowser.qml ├── SnapshotTesting.qml ├── config │ └── snapshot-config.json └── qmldir ├── appveyor.yml ├── dtl ├── Diff.hpp ├── Diff3.hpp ├── Lcs.hpp ├── Sequence.hpp ├── Ses.hpp ├── dtl.hpp ├── functors.hpp └── variables.hpp ├── examples └── example1 │ ├── CustomItem.qml │ ├── README.md │ ├── exmaple1.pro │ ├── main.cpp │ └── tst_demo1.qml ├── private ├── snapshottesting_p.h ├── snapshottestingoptions.h ├── snapshottestingrenderer.h └── snapshottestingtest.h ├── qpm.json ├── snapshots.json ├── snapshottesting.cpp ├── snapshottesting.h ├── snapshottesting.pri ├── snapshottesting.pro ├── snapshottesting.qrc ├── snapshottestingadapter.cpp ├── snapshottestingadapter.h ├── snapshottestingrenderer.cpp ├── snapshottestingrule.cpp ├── snapshottestingtest.cpp └── tests └── snapshottestingunittests ├── .gitignore ├── README.md ├── main.cpp ├── packages ├── QtQuick_Items.qml └── QtQuick_controls_1_2_items.qml ├── qmltests └── tst_SnapshotTesting.qml ├── qpm.json ├── sample ├── CustomButton.qml ├── Sample1.qml ├── Sample2.qml ├── Sample3.qml ├── Sample4.qml ├── Sample5.qml ├── Sample5Form.ui.qml ├── Sample6.qml ├── Sample7.qml ├── Sample8.qml ├── Sample9.qml ├── Sample_Control1.qml ├── Sample_Layout.qml ├── Sample_Loader_Async.qml ├── Sample_QJSValue.qml └── red-100x100.png ├── snapshot-default-values.json ├── snapshots.json ├── snapshottestingunittests.pro ├── testcases.cpp └── testcases.h /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | /.qmake.cache 16 | /.qmake.stash 17 | *.pro.user 18 | *.pro.user.* 19 | *.qbs.user 20 | *.qbs.user.* 21 | *.moc 22 | moc_*.cpp 23 | qrc_*.cpp 24 | ui_*.h 25 | Makefile* 26 | *build-* 27 | 28 | # QtCreator 29 | 30 | *.autosave 31 | 32 | # QtCtreator Qml 33 | *.qmlproject.user 34 | *.qmlproject.user.* 35 | 36 | # QtCtreator CMake 37 | CMakeLists.txt.user 38 | *.qmlc 39 | vendor 40 | *.swp 41 | *.jsc 42 | examples/example1/snapshots.json 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language : cpp 2 | dist: trusty 3 | env: 4 | - DISPLAY=:99.0 5 | addons: 6 | apt: 7 | packages: 8 | - valgrind 9 | compiler: 10 | - gcc 11 | before_install: 12 | - export GOPATH=`pwd`/gosrc 13 | - export PATH=`pwd`/gosrc/bin:$PATH 14 | - go get qpm.io/qpm 15 | - sh -e /etc/init.d/xvfb start 16 | script: 17 | - qpm check 18 | - git clone https://github.com/benlau/qtci.git 19 | - source qtci/path.env 20 | - qt-5.8.0 21 | - source qt-5.8.0.env 22 | - mkdir build 23 | - cd build 24 | - run-unittests ../tests/snapshottestingunittests/snapshottestingunittests.pro 25 | - cd .. 26 | - qpm install 27 | - cd examples/example1 28 | - qmake 29 | - make 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Snapshot Testing 2 | ================ 3 | 4 | This project is a library to offer snapshot testing for QML as a tool to make sure your UI does not change unexpectedly. It is inspired by Jest Snapshot Testing methodology. 5 | 6 | Quoted from [Snapshot Testing · Jest](https://facebook.github.io/jest/docs/snapshot-testing.html) : 7 | 8 | > A typical snapshot test case for a mobile app renders a UI component, takes a screenshot, then compares it to a reference image stored alongside the test. The test will fail if the two images do not match: either the change is unexpected, or the screenshot needs to be updated to the new version of the UI component. 9 | 10 | > A similar approach can be taken when it comes to testing your React components. Instead of rendering the graphical UI, which would require building the entire app, you can use a test renderer to quickly generate a serializable value for your React tree. 11 | 12 | The concept of this project is similar, but it replaces React component by a QObject/QQuickItem instance then convert to a text representation looks similar to QML. Then it compares with the previously stored snapshot. If the snapshots do not match, this library will prompt a dialog to ask the user for confirmation. If the changes are unexcepted, press "No" and will turn the test case fails. Otherwise, pressing "yes" will update the snapshot according to the latest version of the UI component. 13 | 14 | Let’s see a demonstration: 15 | 16 | 17 | ```QML 18 | // tst_Demo1.qml 19 | import QtQuick 2.0 20 | import QtTest 1.0 21 | import SnapshotTesting 1.0 22 | 23 | Item { 24 | id: root 25 | width: 640 26 | height: 480 27 | 28 | CustomItem { 29 | // Don't place this under TestCase object. 30 | id: item1 31 | width: 320 32 | height: 240 33 | anchors.centerIn: parent 34 | } 35 | 36 | TestCase { 37 | name: "Demo1" 38 | when: windowShown 39 | 40 | function test_demo1() { 41 | var snapshot = SnapshotTesting.capture(item1); // Capture "item1" into a text representation 42 | snapshot = snapshot.replace(Qt.resolvedUrl(".."), ""); 43 | SnapshotTesting.matchStoredSnapshot("test_demo1", snapshot); // Compare with previously stored snapshot 44 | } 45 | } 46 | } 47 | ``` 48 | 49 | ```QML 50 | // CustomItem.qml 51 | import QtQuick 2.0 52 | import QtQuick.Layouts 1.3 53 | 54 | Item { 55 | ColumnLayout { 56 | id: column 57 | anchors.fill: parent 58 | spacing: 0 59 | 60 | Rectangle { 61 | Layout.fillHeight: true 62 | Layout.fillWidth: true 63 | color: "#000000" 64 | } 65 | 66 | Rectangle { 67 | Layout.fillHeight: true 68 | Layout.fillWidth: true 69 | color: "#FF0000" 70 | } 71 | 72 | Rectangle { 73 | Layout.fillHeight: true 74 | Layout.fillWidth: true 75 | color: "#FFCC00" 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | In the first time execution, it has no any previously saved snapshot. It will prompt a UI and ask for confirmation of applying the changes to snapshotsFile by the `SnapshotTesting.matchStoredSnapshot` function. 82 | 83 | ![snapshottesting-1.png (1159×552)](https://raw.githubusercontent.com/benlau/junkcode/master/docs/snapshottesting-1.png) 84 | 85 | If you press "No", it will throw an exception to let the test case fails. You should press "Yes" and the snaphosts will be stored. 86 | 87 | Once the snapshots file is created, this UI will not prompt again unless there have changed. For example, if it modify item height from 320 to 180. And run the porgramme again. It will show: 88 | 89 | ![snapshottesting-2.png (655×549)](https://raw.githubusercontent.com/benlau/junkcode/master/docs/snapshottesting-2.png) 90 | 91 | Reference Articles 92 | ------------------ 93 | 94 | 1. [QML Snapshot Testing with TDD – E-Fever – Medium](https://medium.com/e-fever/qml-snapshot-testing-with-tdd-aba81441c52) 95 | 1. [QML Snapshot Testing與TDD的連㩗](http://benlaux.blogspot.hk/2017/08/qml-snapshot-testingtdd_6.html) [Chinese] 96 | 97 | Text Representation 98 | ------------------- 99 | 100 | SnapshotTesting converts a QObject/QQuickItem to a text representation like QML. But it will remove all the data binding/anchors and show the real coordination information. It will also remove non-visible items by default. In case you wish to show non-visual component in your snapshot. You should pass captureVisibleItemOnly to false in the capture() call. 101 | 102 | ``` 103 | var snapshot = SnapshotTesting.capture(item, {captureVisibleItemOnly: false }); 104 | ``` 105 | 106 | 107 | UI Gallery 108 | ---------- 109 | 110 | Quoted from :[The Five Key Mindsets to Master If You Want to Be a Successful Programmer](https://www.effectiveengineer.com/blog/five-key-skills-of-successful-programmers) 111 | 112 | >> Or suppose that you’re fixing a bug that requires you to start the app and then navigate through five screens to set up the right conditions to trigger the bug. Could you spend 10 minutes to wire it up so that it goes to the buggy screen on startup? 113 | 114 | The debugging technique mentioned above is very useful. But there has a problem. How do you manage the code of short cut? If it is not saved in version control, then you have to patch your code for every time you found a new bug. 115 | 116 | However, it is quite difficult to put the short cut code in your application. It will add a lot of `#ifdef` condition that you don’t want to handle it. 117 | 118 | The best place to hold the code is the unit test problem with using SnapshotTesting. You could simulate a specific condition with chosen UI component. The “SnapshotTesting.matchStoredSnapshot()” will only pause the test case but not the UI. So you could evaluate your UI until you have pressed “Yes” to confirm the behaviour. 119 | 120 | This kind of technique has no name. Personally, I would call it as gallery tests. As it will collects a set of UI in different condition finally. 121 | 122 | Installation 123 | ------------ 124 | 125 | ``` 126 | qpm install net.efever.snapshottesting 127 | ``` 128 | 129 | Examples 130 | -------- 131 | 132 | An example program is available in the [examples](https://github.com/e-fever/snapshottesting/tree/master/examples/example1) folder within the source code. 133 | 134 | QML API 135 | --- 136 | 137 | **SnapshotTesting.snapshotsFile[Property]** 138 | 139 | It is a property to hold the file to save/load snapshots. It is recommended to set this property in main.cpp by the C++ API 140 | 141 | **String SnapshotTesting.capture(object, options)** 142 | 143 | This function will capture the data of input object, then convert to a text representation similar to QML. The result truncates data binding/anchors, it will only show visible items and actual values. 144 | 145 | Options 146 | 147 | 1. captureVisibleItemOnly - If this value is set to true, it will only capture visible items. [Default: true] 148 | 1. expandAll - By default, the capture function only captures item in the context of the input object. Set this to true will expand all the nodes. [Default false] 149 | 1. hideId - If this value is set to true, it will not show the "id" field in the captured snapshot. 150 | 151 | **SnapshotTesting.matchStoredSnapshot(name, snapshot)** 152 | 153 | Compare the input snapshot to the previously stored snapshot with the name. If they do not match, it will prompt a dialog to ask for updates or not. If user press "no", it will throw an exception to let the test fails. You should press "Yes" to get the stored snapshot be updated. 154 | 155 | C++ API 156 | ------- 157 | 158 | Header 159 | 160 | ```C++ 161 | #include 162 | ``` 163 | 164 | ``` 165 | void SnapshotTesting::setSnapshotsFile(const QString &file) [static] 166 | ``` 167 | 168 | Set the snapshot file to be saved. 169 | 170 | ```C++ 171 | void SnapshotTesting::setInteractiveEnabled(bool value) [static] 172 | ``` 173 | 174 | If it is set to false, it won't prompt the GUI matcher even the snapshot is not matched. 175 | 176 | 177 | FAQ 178 | ---- 179 | 180 | **Should snapshots file be committed?** 181 | 182 | Yes. 183 | 184 | **Does snapshot testing substitute unit testing?** 185 | 186 | **How do I resolve the conflict within snapshots file?** 187 | 188 | Credits 189 | ------- 190 | 191 | [cubicdaiya/dtl: diff template library written by C++](https://github.com/cubicdaiya/dtl) 192 | -------------------------------------------------------------------------------- /SnapshotTesting/Matcher.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.4 2 | import QtQuick.Controls 1.2 3 | import QtQuick.Dialogs 1.2 4 | 5 | Dialog { 6 | id: component 7 | 8 | visible: false 9 | title: "Snapshot Testing" 10 | modality: Qt.NonModal 11 | 12 | property alias diff: content.diff 13 | property alias previousSnapshot: content.originalVersion 14 | property alias snapshot: content.currentVersion 15 | property alias screenshot: content.screenshot 16 | property alias monospaceFont: content.monospaceFont 17 | property alias previousScreenshot: content.previousScreenshot 18 | 19 | property alias combinedScreenshot: content.combinedScreenshot 20 | 21 | property alias tabIndex: content.tabIndex 22 | 23 | MatcherContentView { 24 | id: content 25 | implicitWidth: 640 26 | implicitHeight: 480 27 | } 28 | 29 | onRejected: { 30 | Qt.quit(); 31 | } 32 | 33 | onAccepted: { 34 | Qt.quit(); 35 | } 36 | 37 | onApply: { 38 | Qt.quit(); 39 | } 40 | 41 | onYes: { 42 | Qt.quit(); 43 | } 44 | 45 | onNo: { 46 | Qt.quit(); 47 | } 48 | 49 | standardButtons: StandardButton.No | StandardButton.Yes | StandardButton.NoToAll | StandardButton.YesToAll 50 | } 51 | -------------------------------------------------------------------------------- /SnapshotTesting/MatcherContentView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Controls 1.4 3 | 4 | Item { 5 | id: contentView 6 | implicitWidth: 640 7 | implicitHeight: 480 8 | 9 | property string diff: "" 10 | property string originalVersion: "" 11 | property string currentVersion: "" 12 | 13 | property string screenshot: "" 14 | property string previousScreenshot: "" 15 | 16 | property string combinedScreenshot: "" 17 | property string monospaceFont: "" 18 | 19 | property alias tabIndex: tabView.currentIndex 20 | 21 | TabView { 22 | id: tabView 23 | anchors.fill: parent 24 | anchors.bottomMargin: 40 25 | clip: true 26 | 27 | Tab { 28 | title: "Diff" 29 | Item { 30 | TextArea { 31 | font.family: monospaceFont 32 | anchors.fill: parent 33 | text: diff 34 | } 35 | } 36 | } 37 | 38 | Tab { 39 | title: "Stored Snapshot" 40 | Item { 41 | TextArea { 42 | font.family: monospaceFont 43 | anchors.fill: parent 44 | text: originalVersion 45 | } 46 | } 47 | } 48 | 49 | Tab { 50 | title: "Current Snapshot" 51 | Item { 52 | TextArea { 53 | font.family: monospaceFont 54 | anchors.fill: parent 55 | text: currentVersion 56 | } 57 | } 58 | } 59 | } 60 | 61 | Component { 62 | id: screenshotViewer 63 | Item { 64 | 65 | ScreenshotBrowser { 66 | anchors.fill: parent 67 | anchors.margins: 4 68 | screenshot: contentView.screenshot 69 | previousScreenshot: contentView.previousScreenshot 70 | combinedScreenshot: contentView.combinedScreenshot 71 | } 72 | } 73 | } 74 | 75 | onScreenshotChanged: { 76 | if (screenshot === "") { 77 | return; 78 | } 79 | 80 | tabView.addTab("Screenshot", screenshotViewer); 81 | } 82 | 83 | Text { 84 | anchors.bottom: parent.bottom 85 | width: parent.width 86 | height: 40 87 | verticalAlignment: Text.AlignVCenter 88 | 89 | leftPadding: 10 90 | rightPadding: 10 91 | text: qsTr("New snapshot does not match the stored snapshot. Inspect your code and press \"Yes\" to update the changes, or press \"No\" to reject.") 92 | 93 | wrapMode: Text.WordWrap 94 | 95 | } 96 | 97 | 98 | } 99 | -------------------------------------------------------------------------------- /SnapshotTesting/ScaleToFitImage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Item { 4 | id: component 5 | 6 | property alias source: image.source 7 | 8 | property alias sourceSize: image.sourceSize 9 | 10 | property alias imageWidth: image.width 11 | property alias imageHeight: image.height 12 | 13 | Image { 14 | id: image 15 | anchors.centerIn: parent 16 | 17 | function refresh() { 18 | if (image.sourceSize.width === 0 || image.sourceSize.height === 0) { 19 | image.scale = 1; 20 | } 21 | 22 | var sw = component.width / image.sourceSize.width; 23 | var sh = component.height / image.sourceSize.height; 24 | 25 | image.scale = Math.min(sw, sh, 1); 26 | } 27 | 28 | onWidthChanged: refresh(); 29 | onHeightChanged: refresh(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SnapshotTesting/ScreenshotBrowser.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.2 3 | import QtQuick.Layouts 1.0 4 | 5 | Item { 6 | id: component 7 | 8 | property string screenshot: "" 9 | 10 | property string previousScreenshot: "" 11 | 12 | property string combinedScreenshot: "" 13 | 14 | function showCombinedScreenshot() { 15 | combinedButton.checked = true; 16 | } 17 | 18 | function base64(data) { 19 | if (data === "") { 20 | return ""; 21 | } 22 | return "data:image/png;base64," + data; 23 | } 24 | 25 | ScaleToFitImage { 26 | id: singleImage 27 | anchors.fill: parent 28 | source: "data:image/png;base64," + screenshot 29 | } 30 | 31 | ColumnLayout { 32 | id: dualImage 33 | anchors.fill: parent 34 | visible: false 35 | enabled: false 36 | 37 | Row { 38 | Layout.fillWidth: true 39 | Layout.fillHeight: true 40 | Layout.maximumHeight: 40 41 | 42 | ExclusiveGroup { id: displayMode } 43 | 44 | RadioButton { 45 | id: sideBySideButton 46 | text: "Side by Side" 47 | checked: true 48 | exclusiveGroup: displayMode 49 | width: 120 50 | height: 20 51 | } 52 | 53 | RadioButton { 54 | id: overlappedButton 55 | text: "Overlapped" 56 | exclusiveGroup: displayMode 57 | width: 120 58 | height: 20 59 | } 60 | 61 | RadioButton { 62 | id: combinedButton 63 | text: "Combined" 64 | objectName: "CombinedButton" 65 | checked: false 66 | visible: combinedScreenshot !== "" 67 | exclusiveGroup: displayMode 68 | width: 120 69 | height: 20 70 | } 71 | } 72 | 73 | Item { 74 | Layout.fillWidth: true 75 | Layout.fillHeight: true 76 | 77 | RowLayout { 78 | id: sideBySideView 79 | visible: sideBySideButton.checked 80 | enabled: visible 81 | anchors.fill: parent 82 | 83 | Item { 84 | Layout.fillHeight: true 85 | Layout.fillWidth: true 86 | ScaleToFitImage { 87 | anchors.fill: parent 88 | anchors.margins: 4 89 | source: { 90 | if (previousScreenshot === "") { 91 | return ""; 92 | } 93 | return "data:image/png;base64," + previousScreenshot; 94 | } 95 | } 96 | } 97 | 98 | Item { 99 | Layout.fillHeight: true 100 | Layout.fillWidth: true 101 | 102 | ScaleToFitImage { 103 | anchors.fill: parent 104 | anchors.margins: 4 105 | source: "data:image/png;base64," + screenshot 106 | } 107 | } 108 | } 109 | 110 | Item { 111 | id: overlappedView 112 | visible: overlappedButton.checked 113 | enabled: visible 114 | anchors.fill: parent 115 | anchors.margins: 4 116 | ScaleToFitImage { 117 | x: 0 118 | y: 0 119 | width: parent.width 120 | height: parent.height 121 | opacity: 0.5 122 | source: base64(previousScreenshot) 123 | 124 | MouseArea { 125 | anchors.fill: parent 126 | drag.target: parent 127 | } 128 | } 129 | 130 | ScaleToFitImage { 131 | x: 0 132 | y: 0 133 | width: parent.width 134 | height: parent.height 135 | opacity: 0.5 136 | source: base64(screenshot) 137 | 138 | MouseArea { 139 | anchors.fill: parent 140 | drag.target: parent 141 | } 142 | } 143 | } 144 | 145 | ScaleToFitImage { 146 | anchors.fill: parent 147 | anchors.margins: 4 148 | visible: combinedButton.checked 149 | enabled: visible 150 | source: { 151 | if (combinedScreenshot === "") { 152 | return ""; 153 | } 154 | return "data:image/png;base64," + combinedScreenshot; 155 | } 156 | } 157 | } 158 | } 159 | 160 | states: [ 161 | State { 162 | name: "SingleMode" 163 | }, 164 | State { 165 | name: "DualMode" 166 | when: previousScreenshot !== "" 167 | 168 | PropertyChanges { 169 | target: singleImage 170 | visible: false 171 | } 172 | 173 | PropertyChanges { 174 | target: dualImage 175 | visible: true 176 | enabled: true 177 | } 178 | }, 179 | State { 180 | name: "CombinedMode" 181 | } 182 | ] 183 | 184 | } 185 | -------------------------------------------------------------------------------- /SnapshotTesting/SnapshotTesting.qml: -------------------------------------------------------------------------------- 1 | pragma Singleton 2 | import QtQuick 2.0 3 | import SnapshotTesting.Private 1.0 4 | 5 | Item { 6 | id: snapshotTesting 7 | 8 | property string snapshotsFile: Adapter.snapshotsFile 9 | 10 | function capture(object, options) { 11 | return Adapter.capture(object, options); 12 | } 13 | 14 | function matchStoredSnapshot(name, snapshot) { 15 | if (!Adapter.matchStoredSnapshot(name,snapshot)) { 16 | throw new Error("matchStoredSnapshot: Current snapshot does not match with stored snapshot"); 17 | } 18 | } 19 | 20 | function _caller() { 21 | try { 22 | throw new Error(); 23 | } catch (e) { 24 | 25 | var lines = e.stack.split("\n"); 26 | var line = lines[1]; 27 | var token = line.split("@"); 28 | 29 | return token[0]; 30 | } 31 | 32 | } 33 | 34 | onSnapshotsFileChanged: { 35 | if (Adapter.snapshotsFile !== snapshotTesting.snapshotsFile) { 36 | Adapter.snapshotsFile = snapshotTesting.snapshotsFile; 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /SnapshotTesting/config/snapshot-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "QObject": { 3 | "name": "QtObject" 4 | }, 5 | "QQuickItem": { 6 | "name": "Item", 7 | "defaultValues": { 8 | "activeFocusOnTab": false, 9 | "antialiasing": false, 10 | "baselineOffset": 0, 11 | "clip": false, 12 | "enabled": true, 13 | "focus": false, 14 | "height": 0, 15 | "implicitHeight": 0, 16 | "implicitWidth": 0, 17 | "objectName": "", 18 | "opacity": 1, 19 | "rotation": 0, 20 | "scale": 1, 21 | "smooth": true, 22 | "state": "", 23 | "visible": true, 24 | "width": 0, 25 | "x": 0, 26 | "y": 0, 27 | "z": 0 28 | } 29 | }, 30 | "QQuickRectangle": { 31 | "name": "Rectangle", 32 | "defaultValues": { 33 | "radius": 0, 34 | "gradient": 0 35 | } 36 | }, 37 | "QQuickText": { 38 | "name": "Text", 39 | "defaultValues": { 40 | "antialiasing": true, 41 | "baselineOffset": 12.56, 42 | "bottomPadding": 0, 43 | "contentHeight": 16, 44 | "contentWidth": 0, 45 | "effectiveHorizontalAlignment": 1, 46 | "elide": 3, 47 | "fontSizeMode": 0, 48 | "horizontalAlignment": 1, 49 | "hoveredLink": "", 50 | "implicitHeight": 16, 51 | "leftPadding": 0, 52 | "lineCount": 1, 53 | "lineHeight": 1, 54 | "lineHeightMode": 0, 55 | "maximumLineCount": 2147483647, 56 | "minimumPixelSize": 12, 57 | "minimumPointSize": 12, 58 | "padding": 0, 59 | "paintedHeight": 16, 60 | "paintedWidth": 0, 61 | "renderType": 0, 62 | "rightPadding": 0, 63 | "style": 0, 64 | "text": "", 65 | "textFormat": 2, 66 | "topPadding": 0, 67 | "truncated": false, 68 | "verticalAlignment": 32, 69 | "wrapMode": 0 70 | } 71 | }, 72 | "QQuickColumn": { 73 | "name" : "Column", 74 | "defaultValues": { 75 | "leftPadding": 0, 76 | "rightPadding": 0, 77 | "topPadding" : 0, 78 | "bottomPadding": 0, 79 | "spacing" : 0, 80 | "padding" : 0 81 | } 82 | }, 83 | "QQuickRepeater": { 84 | "defaultValues": { 85 | "count": 0 86 | } 87 | }, 88 | "QQuickImage": { 89 | "defaultValues": { 90 | "source": "" 91 | } 92 | }, 93 | "QQuickButton": { 94 | "defaultValues": { 95 | } 96 | }, 97 | "QQuickMouseArea": { 98 | }, 99 | "RadioButton@QtQuick.Controls": { 100 | }, 101 | "packages": { 102 | "QtQuick": [ 103 | ["QQuickMouseArea", "MouseArea"], 104 | ["QQuickImage", "Image"], 105 | ["QQuickItem", "Item"], 106 | ["QQuickText", "Text"], 107 | ["QQuickAnimatedImage", "AnimatedImage"], 108 | ["QQuickBorderImage", "BorderImage"], 109 | ["QQuickDropArea", "DropArea"], 110 | ["QQuickFlickable", "Flickable"], 111 | ["QQuickFocusScope", "FocusScope"], 112 | ["QQuickGridView", "GridView"], 113 | ["QQuickListView", "ListView"], 114 | ["QQuickPinchArea", "PinchArea"], 115 | ["QQuickRow", "Row"], 116 | ["QQuickColumn", "Column"] 117 | ] 118 | }, 119 | "rules": [ 120 | "Item@QtQuick::implicitWidth", 121 | "Item@QtQuick::implicitHeight", 122 | "Item@QtQuick::childrenRect", 123 | "Item@QtQuick::parent", 124 | "Item@QtQuick::transformOrigin", 125 | "Item@QtQuick::transitions", 126 | "Item@QtQuick::verticalCenter", 127 | "Item@QtQuick::visibleChildren", 128 | "Item@QtQuick::states", 129 | "Item@QtQuick::right", 130 | "Item@QtQuick::left", 131 | "Item@QtQuick::top", 132 | "Item@QtQuick::bottom", 133 | "Item@QtQuick::resources", 134 | "Item@QtQuick::transform", 135 | "Item@QtQuick::data", 136 | "Item@QtQuick::horizontalCenter", 137 | "Item@QtQuick::children", 138 | "Item@QtQuick::transformOriginPoint", 139 | "Item@QtQuick::status", 140 | "Item@QtQuick::baseline", 141 | "Item@QtQuick::focus", 142 | "Item@QtQuick::activeFocus", 143 | "Item@QtQuick::anchors", 144 | "Item@QtQuick::layer", 145 | "Text@QtQuick::paintedHeight", 146 | "Text@QtQuick::paintedWidth", 147 | "Text@QtQuick::baselineOffset", 148 | "Text@QtQuick::contentHeight", 149 | "Text@QtQuick::baseUrl", 150 | "Text@QtQuick::baselineOffset", 151 | "Text@QtQuick::implicitHeight", 152 | "Text@QtQuick::implicitWidth", 153 | "Text@QtQuick::fontInfo", 154 | "Image@QtQuick::paintedHeight", 155 | "Image@QtQuick::paintedWidth", 156 | "Image@QtQuick::status", 157 | "MouseArea@QtQuick::mouseX", 158 | "MouseArea@QtQuick::mouseY", 159 | "MouseArea@QtQuick::drag", 160 | "Button@QtQuick.Controls::__effectivePressed", 161 | "Button@QtQuick.Controls::__iconOverriden", 162 | "Button@QtQuick.Controls::__position", 163 | "Button@QtQuick.Controls::activeFocusOnPress", 164 | "Button@QtQuick.Controls::activeFocusOnTab", 165 | "Button@QtQuick.Controls::baselineOffset", 166 | "RadioButton@QtQuick.Controls::baselineOffset" 167 | ] 168 | } 169 | -------------------------------------------------------------------------------- /SnapshotTesting/qmldir: -------------------------------------------------------------------------------- 1 | singleton SnapshotTesting 1.0 SnapshotTesting.qml 2 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: build{build} 2 | 3 | branches: 4 | except: 5 | - project/travis 6 | 7 | environment: 8 | matrix: 9 | - name: win32 10 | platform: amd64_x86 11 | qt: msvc2017 12 | APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 13 | 14 | build_script: 15 | - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" 16 | - set GOPATH=c:\gopath 17 | - set QTDIR=C:\Qt\5.9.7\msvc2017_64 18 | - set PATH=%PATH%;%QTDIR%\bin;C:\MinGW\bin;%GOPATH%\bin; 19 | - go get qpm.io/qpm 20 | - go install qpm.io/qpm 21 | - dir %GOPATH%\bin 22 | - cd tests/snapshottestingunittests/ 23 | - qpm install 24 | - qmake snapshottestingunittests.pro 25 | - nmake 26 | - dir /w 27 | - dir release /w 28 | - release\snapshottesting 29 | -------------------------------------------------------------------------------- /dtl/Diff.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | dtl -- Diff Template Library 3 | 4 | In short, Diff Template Library is distributed under so called "BSD license", 5 | 6 | Copyright (c) 2015 Tatsuhiko Kubo 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of the authors nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | /* If you use this library, you must include dtl.hpp only. */ 37 | 38 | #ifndef DTL_DIFF_H 39 | #define DTL_DIFF_H 40 | 41 | namespace dtl { 42 | 43 | /** 44 | * diff class template 45 | * sequence must support random_access_iterator. 46 | */ 47 | template , typename comparator = Compare< elem > > 48 | class Diff 49 | { 50 | private : 51 | dtl_typedefs(elem, sequence) 52 | sequence A; 53 | sequence B; 54 | size_t M; 55 | size_t N; 56 | size_t delta; 57 | size_t offset; 58 | long long *fp; 59 | long long editDistance; 60 | Lcs< elem > lcs; 61 | Ses< elem > ses; 62 | editPath path; 63 | editPathCordinates pathCordinates; 64 | bool swapped; 65 | bool huge; 66 | bool trivial; 67 | bool editDistanceOnly; 68 | uniHunkVec uniHunks; 69 | comparator cmp; 70 | public : 71 | Diff () {} 72 | 73 | Diff (const sequence& a, 74 | const sequence& b) : A(a), B(b), ses(false) { 75 | init(); 76 | } 77 | 78 | Diff (const sequence& a, 79 | const sequence& b, 80 | bool deletesFirst) : A(a), B(b), ses(deletesFirst) { 81 | init(); 82 | } 83 | 84 | Diff (const sequence& a, 85 | const sequence& b, 86 | const comparator& comp) : A(a), B(b), ses(false), cmp(comp) { 87 | init(); 88 | } 89 | 90 | Diff (const sequence& a, 91 | const sequence& b, 92 | bool deleteFirst, 93 | const comparator& comp) : A(a), B(b), ses(deleteFirst), cmp(comp) { 94 | init(); 95 | } 96 | 97 | ~Diff() {} 98 | 99 | long long getEditDistance () const { 100 | return editDistance; 101 | } 102 | 103 | Lcs< elem > getLcs () const { 104 | return lcs; 105 | } 106 | 107 | elemVec getLcsVec () const { 108 | return lcs.getSequence(); 109 | } 110 | 111 | Ses< elem > getSes () const { 112 | return ses; 113 | } 114 | 115 | uniHunkVec getUniHunks () const { 116 | return uniHunks; 117 | } 118 | 119 | /* These should be deprecated */ 120 | bool isHuge () const { 121 | return huge; 122 | } 123 | 124 | void onHuge () { 125 | this->huge = true; 126 | } 127 | 128 | void offHuge () { 129 | this->huge = false; 130 | } 131 | 132 | bool isUnserious () const { 133 | return trivial; 134 | } 135 | 136 | void onUnserious () { 137 | this->trivial = true; 138 | } 139 | 140 | void offUnserious () { 141 | this->trivial = false; 142 | } 143 | 144 | void onOnlyEditDistance () { 145 | this->editDistanceOnly = true; 146 | } 147 | 148 | /* These are the replacements for the above */ 149 | bool hugeEnabled () const { 150 | return huge; 151 | } 152 | 153 | void enableHuge () { 154 | this->huge = true; 155 | } 156 | 157 | void disableHuge () { 158 | this->huge = false; 159 | } 160 | 161 | bool trivialEnabled () const { 162 | return trivial; 163 | } 164 | 165 | void enableTrivial () const { 166 | this->trivial = true; 167 | } 168 | 169 | void disableTrivial () { 170 | this->trivial = false; 171 | } 172 | 173 | void editDistanceOnlyEnabled () { 174 | this->editDistanceOnly = true; 175 | } 176 | 177 | /** 178 | * patching with Unified Format Hunks 179 | */ 180 | sequence uniPatch (const sequence& seq) { 181 | elemList seqLst(seq.begin(), seq.end()); 182 | sesElemVec shunk; 183 | sesElemVec_iter vsesIt; 184 | elemList_iter lstIt = seqLst.begin(); 185 | long long inc_dec_total = 0; 186 | long long gap = 1; 187 | for (uniHunkVec_iter it=uniHunks.begin();it!=uniHunks.end();++it) { 188 | joinSesVec(shunk, it->common[0]); 189 | joinSesVec(shunk, it->change); 190 | joinSesVec(shunk, it->common[1]); 191 | it->a += inc_dec_total; 192 | inc_dec_total += it->inc_dec_count; 193 | for (long long i=0;ia - gap;++i) { 194 | ++lstIt; 195 | } 196 | gap = it->a + it->b + it->inc_dec_count; 197 | vsesIt = shunk.begin(); 198 | while (vsesIt!=shunk.end()) { 199 | switch (vsesIt->second.type) { 200 | case SES_ADD : 201 | seqLst.insert(lstIt, vsesIt->first); 202 | break; 203 | case SES_DELETE : 204 | if (lstIt != seqLst.end()) { 205 | lstIt = seqLst.erase(lstIt); 206 | } 207 | break; 208 | case SES_COMMON : 209 | if (lstIt != seqLst.end()) { 210 | ++lstIt; 211 | } 212 | break; 213 | default : 214 | // no fall-through 215 | break; 216 | } 217 | ++vsesIt; 218 | } 219 | shunk.clear(); 220 | } 221 | 222 | sequence patchedSeq(seqLst.begin(), seqLst.end()); 223 | return patchedSeq; 224 | } 225 | 226 | /** 227 | * patching with Shortest Edit Script (SES) 228 | */ 229 | sequence patch (const sequence& seq) const { 230 | sesElemVec sesSeq = ses.getSequence(); 231 | elemList seqLst(seq.begin(), seq.end()); 232 | elemList_iter lstIt = seqLst.begin(); 233 | for (sesElemVec_iter sesIt=sesSeq.begin();sesIt!=sesSeq.end();++sesIt) { 234 | switch (sesIt->second.type) { 235 | case SES_ADD : 236 | seqLst.insert(lstIt, sesIt->first); 237 | break; 238 | case SES_DELETE : 239 | lstIt = seqLst.erase(lstIt); 240 | break; 241 | case SES_COMMON : 242 | ++lstIt; 243 | break; 244 | default : 245 | // no through 246 | break; 247 | } 248 | } 249 | sequence patchedSeq(seqLst.begin(), seqLst.end()); 250 | return patchedSeq; 251 | } 252 | 253 | /** 254 | * compose Longest Common Subsequence and Shortest Edit Script. 255 | * The algorithm implemented here is based on "An O(NP) Sequence Comparison Algorithm" 256 | * described by Sun Wu, Udi Manber and Gene Myers 257 | */ 258 | void compose() { 259 | 260 | if (isHuge()) { 261 | pathCordinates.reserve(MAX_CORDINATES_SIZE); 262 | } 263 | 264 | long long p = -1; 265 | fp = new long long[M + N + 3]; 266 | fill(&fp[0], &fp[M + N + 3], -1); 267 | path = editPath(M + N + 3); 268 | fill(path.begin(), path.end(), -1); 269 | ONP: 270 | do { 271 | ++p; 272 | for (long long k=-p;k<=static_cast(delta)-1;++k) { 273 | fp[k+offset] = snake(k, fp[k-1+offset]+1, fp[k+1+offset]); 274 | } 275 | for (long long k=static_cast(delta)+p;k>=static_cast(delta)+1;--k) { 276 | fp[k+offset] = snake(k, fp[k-1+offset]+1, fp[k+1+offset]); 277 | } 278 | fp[delta+offset] = snake(static_cast(delta), fp[delta-1+offset]+1, fp[delta+1+offset]); 279 | } while (fp[delta+offset] != static_cast(N) && pathCordinates.size() < MAX_CORDINATES_SIZE); 280 | 281 | editDistance += static_cast(delta) + 2 * p; 282 | long long r = path[delta+offset]; 283 | P cordinate; 284 | editPathCordinates epc(0); 285 | 286 | // recording edit distance only 287 | if (editDistanceOnly) { 288 | delete[] this->fp; 289 | return; 290 | } 291 | 292 | while(r != -1) { 293 | cordinate.x = pathCordinates[(size_t)r].x; 294 | cordinate.y = pathCordinates[(size_t)r].y; 295 | epc.push_back(cordinate); 296 | r = pathCordinates[(size_t)r].k; 297 | } 298 | 299 | // record Longest Common Subsequence & Shortest Edit Script 300 | if (!recordSequence(epc)) { 301 | pathCordinates.resize(0); 302 | epc.resize(0); 303 | p = -1; 304 | goto ONP; 305 | } 306 | delete[] this->fp; 307 | } 308 | 309 | /** 310 | * print difference between A and B as an SES 311 | */ 312 | template < typename stream > 313 | void printSES (stream& out) const { 314 | sesElemVec ses_v = ses.getSequence(); 315 | for_each(ses_v.begin(), ses_v.end(), ChangePrinter< sesElem, stream >(out)); 316 | } 317 | 318 | void printSES (ostream& out = cout) const { 319 | printSES< ostream >(out); 320 | } 321 | 322 | /** 323 | * print differences given an SES 324 | */ 325 | template < typename stream > 326 | static void printSES (const Ses< elem >& s, stream& out) { 327 | sesElemVec ses_v = s.getSequence(); 328 | for_each(ses_v.begin(), ses_v.end(), ChangePrinter< sesElem, stream >(out)); 329 | } 330 | 331 | static void printSES (const Ses< elem >& s, ostream& out = cout) { 332 | printSES< ostream >(s, out); 333 | } 334 | 335 | /** 336 | * print difference between A and B as an SES with custom printer 337 | */ 338 | template < typename stream, template < typename SEET, typename STRT > class PT > 339 | void printSES (stream& out) const { 340 | sesElemVec ses_v = ses.getSequence (); 341 | for_each (ses_v.begin (), ses_v.end(), PT < sesElem, stream > (out)); 342 | } 343 | 344 | /** 345 | * print difference between A and B in the Unified Format 346 | */ 347 | template < typename stream > 348 | void printUnifiedFormat (stream& out) const { 349 | for_each(uniHunks.begin(), uniHunks.end(), UniHunkPrinter< sesElem, stream >(out)); 350 | } 351 | 352 | void printUnifiedFormat (ostream& out = cout) const { 353 | printUnifiedFormat< ostream >(out); 354 | } 355 | 356 | /** 357 | * print unified format difference with given unified format hunks 358 | */ 359 | template < typename stream > 360 | static void printUnifiedFormat (const uniHunkVec& hunks, stream& out) { 361 | for_each(hunks.begin(), hunks.end(), UniHunkPrinter< sesElem >(out)); 362 | } 363 | 364 | static void printUnifiedFormat (const uniHunkVec& hunks, ostream& out = cout) { 365 | printUnifiedFormat< ostream >(hunks, out); 366 | } 367 | 368 | /** 369 | * compose Unified Format Hunks from Shortest Edit Script 370 | */ 371 | void composeUnifiedHunks () { 372 | sesElemVec common[2]; 373 | sesElemVec change; 374 | sesElemVec ses_v = ses.getSequence(); 375 | long long l_cnt = 1; 376 | long long length = distance(ses_v.begin(), ses_v.end()); 377 | long long middle = 0; 378 | bool isMiddle, isAfter; 379 | elemInfo einfo; 380 | long long a, b, c, d; // @@ -a,b +c,d @@ 381 | long long inc_dec_count = 0; 382 | uniHunk< sesElem > hunk; 383 | sesElemVec adds; 384 | sesElemVec deletes; 385 | 386 | isMiddle = isAfter = false; 387 | a = b = c = d = 0; 388 | 389 | for (sesElemVec_iter it=ses_v.begin();it!=ses_v.end();++it, ++l_cnt) { 390 | einfo = it->second; 391 | switch (einfo.type) { 392 | case SES_ADD : 393 | middle = 0; 394 | ++inc_dec_count; 395 | adds.push_back(*it); 396 | if (!isMiddle) isMiddle = true; 397 | if (isMiddle) ++d; 398 | if (l_cnt >= length) { 399 | joinSesVec(change, deletes); 400 | joinSesVec(change, adds); 401 | isAfter = true; 402 | } 403 | break; 404 | case SES_DELETE : 405 | middle = 0; 406 | --inc_dec_count; 407 | deletes.push_back(*it); 408 | if (!isMiddle) isMiddle = true; 409 | if (isMiddle) ++b; 410 | if (l_cnt >= length) { 411 | joinSesVec(change, deletes); 412 | joinSesVec(change, adds); 413 | isAfter = true; 414 | } 415 | break; 416 | case SES_COMMON : 417 | ++b;++d; 418 | if (common[1].empty() && adds.empty() && deletes.empty() && change.empty()) { 419 | if (static_cast(common[0].size()) < DTL_CONTEXT_SIZE) { 420 | if (a == 0 && c == 0) { 421 | if (!wasSwapped()) { 422 | a = einfo.beforeIdx; 423 | c = einfo.afterIdx; 424 | } else { 425 | a = einfo.afterIdx; 426 | c = einfo.beforeIdx; 427 | } 428 | } 429 | common[0].push_back(*it); 430 | } else { 431 | rotate(common[0].begin(), common[0].begin() + 1, common[0].end()); 432 | common[0].pop_back(); 433 | common[0].push_back(*it); 434 | ++a;++c; 435 | --b;--d; 436 | } 437 | } 438 | if (isMiddle && !isAfter) { 439 | ++middle; 440 | joinSesVec(change, deletes); 441 | joinSesVec(change, adds); 442 | change.push_back(*it); 443 | if (middle >= DTL_SEPARATE_SIZE || l_cnt >= length) { 444 | isAfter = true; 445 | } 446 | adds.clear(); 447 | deletes.clear(); 448 | } 449 | break; 450 | default : 451 | // no through 452 | break; 453 | } 454 | // compose unified format hunk 455 | if (isAfter && !change.empty()) { 456 | sesElemVec_iter cit = it; 457 | long long cnt = 0; 458 | for (long long i=0;isecond.type == SES_COMMON) { 460 | ++cnt; 461 | } 462 | } 463 | if (cnt < DTL_SEPARATE_SIZE && l_cnt < length) { 464 | middle = 0; 465 | isAfter = false; 466 | continue; 467 | } 468 | if (static_cast(common[0].size()) >= DTL_SEPARATE_SIZE) { 469 | long long c0size = static_cast(common[0].size()); 470 | rotate(common[0].begin(), 471 | common[0].begin() + (size_t)c0size - DTL_SEPARATE_SIZE, 472 | common[0].end()); 473 | for (long long i=0;i 507 | static Ses< elem > composeSesFromStream (stream& st) 508 | { 509 | elem line; 510 | Ses< elem > ret; 511 | long long x_idx, y_idx; 512 | x_idx = y_idx = 1; 513 | while (getline(st, line)) { 514 | elem mark(line.begin(), line.begin() + 1); 515 | elem e(line.begin() + 1, line.end()); 516 | if (mark == SES_MARK_DELETE) { 517 | ret.addSequence(e, x_idx, 0, SES_DELETE); 518 | ++x_idx; 519 | } else if (mark == SES_MARK_ADD) { 520 | ret.addSequence(e, y_idx, 0, SES_ADD); 521 | ++y_idx; 522 | } else if (mark == SES_MARK_COMMON) { 523 | ret.addSequence(e, x_idx, y_idx, SES_COMMON); 524 | ++x_idx; 525 | ++y_idx; 526 | } 527 | } 528 | return ret; 529 | } 530 | 531 | private : 532 | /** 533 | * initialize 534 | */ 535 | void init () { 536 | M = distance(A.begin(), A.end()); 537 | N = distance(B.begin(), B.end()); 538 | if (M < N) { 539 | swapped = false; 540 | } else { 541 | swap(A, B); 542 | swap(M, N); 543 | swapped = true; 544 | } 545 | editDistance = 0; 546 | delta = N - M; 547 | offset = M + 1; 548 | huge = false; 549 | trivial = false; 550 | editDistanceOnly = false; 551 | fp = NULL; 552 | } 553 | 554 | /** 555 | * search shortest path and record the path 556 | */ 557 | long long snake(const long long& k, const long long& above, const long long& below) { 558 | long long r = above > below ? path[(size_t)k-1+offset] : path[(size_t)k+1+offset]; 559 | long long y = max(above, below); 560 | long long x = y - k; 561 | while ((size_t)x < M && (size_t)y < N && (swapped ? cmp.impl(B[(size_t)y], A[(size_t)x]) : cmp.impl(A[(size_t)x], B[(size_t)y]))) { 562 | ++x;++y; 563 | } 564 | 565 | path[(size_t)k+offset] = static_cast(pathCordinates.size()); 566 | if (!editDistanceOnly) { 567 | P p; 568 | p.x = x;p.y = y;p.k = r; 569 | pathCordinates.push_back(p); 570 | } 571 | return y; 572 | } 573 | 574 | /** 575 | * record SES and LCS 576 | */ 577 | bool recordSequence (const editPathCordinates& v) { 578 | sequence_const_iter x(A.begin()); 579 | sequence_const_iter y(B.begin()); 580 | long long x_idx, y_idx; // line number for Unified Format 581 | long long px_idx, py_idx; // cordinates 582 | bool complete = false; 583 | x_idx = y_idx = 1; 584 | px_idx = py_idx = 0; 585 | for (size_t i=v.size()-1;!complete;--i) { 586 | while(px_idx < v[i].x || py_idx < v[i].y) { 587 | if (v[i].y - v[i].x > py_idx - px_idx) { 588 | if (!wasSwapped()) { 589 | ses.addSequence(*y, 0, y_idx, SES_ADD); 590 | } else { 591 | ses.addSequence(*y, y_idx, 0, SES_DELETE); 592 | } 593 | ++y; 594 | ++y_idx; 595 | ++py_idx; 596 | } else if (v[i].y - v[i].x < py_idx - px_idx) { 597 | if (!wasSwapped()) { 598 | ses.addSequence(*x, x_idx, 0, SES_DELETE); 599 | } else { 600 | ses.addSequence(*x, 0, x_idx, SES_ADD); 601 | } 602 | ++x; 603 | ++x_idx; 604 | ++px_idx; 605 | } else { 606 | if (!wasSwapped()) { 607 | lcs.addSequence(*x); 608 | ses.addSequence(*x, x_idx, y_idx, SES_COMMON); 609 | } else { 610 | lcs.addSequence(*y); 611 | ses.addSequence(*y, y_idx, x_idx, SES_COMMON); 612 | } 613 | ++x; 614 | ++y; 615 | ++x_idx; 616 | ++y_idx; 617 | ++px_idx; 618 | ++py_idx; 619 | } 620 | } 621 | if (i == 0) complete = true; 622 | } 623 | 624 | if (x_idx > static_cast(M) && y_idx > static_cast(N)) { 625 | // all recording succeeded 626 | } else { 627 | // trivial difference 628 | if (trivialEnabled()) { 629 | if (!wasSwapped()) { 630 | recordOddSequence(x_idx, M, x, SES_DELETE); 631 | recordOddSequence(y_idx, N, y, SES_ADD); 632 | } else { 633 | recordOddSequence(x_idx, M, x, SES_ADD); 634 | recordOddSequence(y_idx, N, y, SES_DELETE); 635 | } 636 | return true; 637 | } 638 | 639 | // nontrivial difference 640 | sequence A_(A.begin() + (size_t)x_idx - 1, A.end()); 641 | sequence B_(B.begin() + (size_t)y_idx - 1, B.end()); 642 | A = A_; 643 | B = B_; 644 | M = distance(A.begin(), A.end()); 645 | N = distance(B.begin(), B.end()); 646 | delta = N - M; 647 | offset = M + 1; 648 | delete[] fp; 649 | fp = new long long[M + N + 3]; 650 | fill(&fp[0], &fp[M + N + 3], -1); 651 | fill(path.begin(), path.end(), -1); 652 | return false; 653 | } 654 | return true; 655 | } 656 | 657 | /** 658 | * record odd sequence in SES 659 | */ 660 | void inline recordOddSequence (long long idx, long long length, sequence_const_iter it, const edit_t et) { 661 | while(idx < length){ 662 | ses.addSequence(*it, idx, 0, et); 663 | ++it; 664 | ++idx; 665 | ++editDistance; 666 | } 667 | ses.addSequence(*it, idx, 0, et); 668 | ++editDistance; 669 | } 670 | 671 | /** 672 | * join SES vectors 673 | */ 674 | void inline joinSesVec (sesElemVec& s1, sesElemVec& s2) const { 675 | if (!s2.empty()) { 676 | for (sesElemVec_iter vit=s2.begin();vit!=s2.end();++vit) { 677 | s1.push_back(*vit); 678 | } 679 | } 680 | } 681 | 682 | /** 683 | * check if the sequences have been swapped 684 | */ 685 | bool inline wasSwapped () const { 686 | return swapped; 687 | } 688 | 689 | }; 690 | } 691 | 692 | #endif // DTL_DIFF_H 693 | -------------------------------------------------------------------------------- /dtl/Diff3.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | dtl -- Diff Template Library 3 | 4 | In short, Diff Template Library is distributed under so called "BSD license", 5 | 6 | Copyright (c) 2015 Tatsuhiko Kubo 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of the authors nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | /* If you use this library, you must include dtl.hpp only. */ 37 | 38 | #ifndef DTL_DIFF3_H 39 | #define DTL_DIFF3_H 40 | 41 | namespace dtl { 42 | 43 | /** 44 | * diff3 class template 45 | * sequence must support random_access_iterator. 46 | */ 47 | template , typename comparator = Compare< elem > > 48 | class Diff3 49 | { 50 | private: 51 | dtl_typedefs(elem, sequence) 52 | sequence A; 53 | sequence B; 54 | sequence C; 55 | sequence S; 56 | Diff< elem, sequence, comparator > diff_ba; 57 | Diff< elem, sequence, comparator > diff_bc; 58 | bool conflict; 59 | elem csepabegin; 60 | elem csepa; 61 | elem csepaend; 62 | public : 63 | Diff3 () {} 64 | Diff3 (const sequence& a, 65 | const sequence& b, 66 | const sequence& c) : A(a), B(b), C(c), 67 | diff_ba(b, a), diff_bc(b, c), 68 | conflict(false) {} 69 | 70 | ~Diff3 () {} 71 | 72 | bool isConflict () const { 73 | return conflict; 74 | } 75 | 76 | sequence getMergedSequence () const { 77 | return S; 78 | } 79 | 80 | /** 81 | * merge changes B and C into A 82 | */ 83 | bool merge () { 84 | if (diff_ba.getEditDistance() == 0) { // A == B 85 | if (diff_bc.getEditDistance() == 0) { // A == B == C 86 | S = B; 87 | return true; 88 | } 89 | S = C; 90 | return true; 91 | } else { // A != B 92 | if (diff_bc.getEditDistance() == 0) { // A != B == C 93 | S = A; 94 | return true; 95 | } else { // A != B != C 96 | S = merge_(); 97 | if (isConflict()) { // conflict occured 98 | return false; 99 | } 100 | } 101 | } 102 | return true; 103 | } 104 | 105 | /** 106 | * compose differences 107 | */ 108 | void compose () { 109 | diff_ba.compose(); 110 | diff_bc.compose(); 111 | } 112 | 113 | private : 114 | /** 115 | * merge implementation 116 | */ 117 | sequence merge_ () { 118 | elemVec seq; 119 | Ses< elem > ses_ba = diff_ba.getSes(); 120 | Ses< elem > ses_bc = diff_bc.getSes(); 121 | sesElemVec ses_ba_v = ses_ba.getSequence(); 122 | sesElemVec ses_bc_v = ses_bc.getSequence(); 123 | sesElemVec_iter ba_it = ses_ba_v.begin(); 124 | sesElemVec_iter bc_it = ses_bc_v.begin(); 125 | sesElemVec_iter ba_end = ses_ba_v.end(); 126 | sesElemVec_iter bc_end = ses_bc_v.end(); 127 | 128 | while (!isEnd(ba_end, ba_it) || !isEnd(bc_end, bc_it)) { 129 | while (true) { 130 | if (!isEnd(ba_end, ba_it) && 131 | !isEnd(bc_end, bc_it) && 132 | ba_it->first == bc_it->first && 133 | ba_it->second.type == SES_COMMON && 134 | bc_it->second.type == SES_COMMON) { 135 | // do nothing 136 | } else { 137 | break; 138 | } 139 | if (!isEnd(ba_end, ba_it)) seq.push_back(ba_it->first); 140 | else if (!isEnd(bc_end, bc_it)) seq.push_back(bc_it->first); 141 | forwardUntilEnd(ba_end, ba_it); 142 | forwardUntilEnd(bc_end, bc_it); 143 | } 144 | if (isEnd(ba_end, ba_it) || isEnd(bc_end, bc_it)) break; 145 | if ( ba_it->second.type == SES_COMMON 146 | && bc_it->second.type == SES_DELETE) { 147 | forwardUntilEnd(ba_end, ba_it); 148 | forwardUntilEnd(bc_end, bc_it); 149 | } else if (ba_it->second.type == SES_COMMON && 150 | bc_it->second.type == SES_ADD) { 151 | seq.push_back(bc_it->first); 152 | forwardUntilEnd(bc_end, bc_it); 153 | } else if (ba_it->second.type == SES_DELETE && 154 | bc_it->second.type == SES_COMMON) { 155 | forwardUntilEnd(ba_end, ba_it); 156 | forwardUntilEnd(bc_end, bc_it); 157 | } else if (ba_it->second.type == SES_DELETE && 158 | bc_it->second.type == SES_DELETE) { 159 | if (ba_it->first == bc_it->first) { 160 | forwardUntilEnd(ba_end, ba_it); 161 | forwardUntilEnd(bc_end, bc_it); 162 | } else { 163 | // conflict 164 | conflict = true; 165 | return B; 166 | } 167 | } else if (ba_it->second.type == SES_DELETE && 168 | bc_it->second.type == SES_ADD) { 169 | // conflict 170 | conflict = true; 171 | return B; 172 | } else if (ba_it->second.type == SES_ADD && 173 | bc_it->second.type == SES_COMMON) { 174 | seq.push_back(ba_it->first); 175 | forwardUntilEnd(ba_end, ba_it); 176 | } else if (ba_it->second.type == SES_ADD && 177 | bc_it->second.type == SES_DELETE) { 178 | // conflict 179 | conflict = true; 180 | return B; 181 | } else if (ba_it->second.type == SES_ADD && 182 | bc_it->second.type == SES_ADD) { 183 | if (ba_it->first == bc_it->first) { 184 | seq.push_back(ba_it->first); 185 | forwardUntilEnd(ba_end, ba_it); 186 | forwardUntilEnd(bc_end, bc_it); 187 | } else { 188 | // conflict 189 | conflict = true; 190 | return B; 191 | } 192 | } 193 | } 194 | 195 | if (isEnd(ba_end, ba_it)) { 196 | addDecentSequence(bc_end, bc_it, seq); 197 | } else if (isEnd(bc_end, bc_it)) { 198 | addDecentSequence(ba_end, ba_it, seq); 199 | } 200 | 201 | sequence mergedSeq(seq.begin(), seq.end()); 202 | return mergedSeq; 203 | } 204 | 205 | /** 206 | * join elem vectors 207 | */ 208 | void inline joinElemVec (elemVec& s1, elemVec& s2) const { 209 | if (!s2.empty()) { 210 | for (elemVec_iter vit=s2.begin();vit!=s2.end();++vit) { 211 | s1.push_back(*vit); 212 | } 213 | } 214 | } 215 | 216 | /** 217 | * check if sequence is at end 218 | */ 219 | template 220 | bool inline isEnd (const T_iter& end, const T_iter& it) const { 221 | return it == end ? true : false; 222 | } 223 | 224 | /** 225 | * increment iterator until iterator is at end 226 | */ 227 | template 228 | void inline forwardUntilEnd (const T_iter& end, T_iter& it) const { 229 | if (!isEnd(end, it)) ++it; 230 | } 231 | 232 | /** 233 | * add elements whose SES's type is ADD 234 | */ 235 | void inline addDecentSequence (const sesElemVec_iter& end, sesElemVec_iter& it, elemVec& seq) const { 236 | while (!isEnd(end, it)) { 237 | if (it->second.type == SES_ADD) seq.push_back(it->first); 238 | ++it; 239 | } 240 | } 241 | 242 | }; 243 | } 244 | 245 | #endif // DTL_DIFF3_H 246 | -------------------------------------------------------------------------------- /dtl/Lcs.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | dtl -- Diff Template Library 3 | 4 | In short, Diff Template Library is distributed under so called "BSD license", 5 | 6 | Copyright (c) 2015 Tatsuhiko Kubo 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of the authors nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | /* If you use this library, you must include dtl.hpp only. */ 37 | 38 | #ifndef DTL_LCS_H 39 | #define DTL_LCS_H 40 | 41 | namespace dtl { 42 | 43 | /** 44 | * Longest Common Subsequence template class 45 | */ 46 | template 47 | class Lcs : public Sequence< elem > 48 | { 49 | public : 50 | Lcs () {} 51 | ~Lcs () {} 52 | }; 53 | } 54 | 55 | #endif // DTL_LCS_H 56 | -------------------------------------------------------------------------------- /dtl/Sequence.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | dtl -- Diff Template Library 3 | 4 | In short, Diff Template Library is distributed under so called "BSD license", 5 | 6 | Copyright (c) 2015 Tatsuhiko Kubo 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of the authors nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | /* If you use this library, you must include dtl.hpp only. */ 37 | 38 | #ifndef DTL_SEQUENCE_H 39 | #define DTL_SEQUENCE_H 40 | 41 | namespace dtl { 42 | 43 | /** 44 | * sequence class template 45 | */ 46 | template 47 | class Sequence 48 | { 49 | public : 50 | typedef vector< elem > elemVec; 51 | Sequence () {} 52 | virtual ~Sequence () {} 53 | 54 | elemVec getSequence () const { 55 | return sequence; 56 | } 57 | void addSequence (elem e) { 58 | sequence.push_back(e); 59 | } 60 | protected : 61 | elemVec sequence; 62 | }; 63 | } 64 | 65 | #endif // DTL_SEQUENCE_H 66 | -------------------------------------------------------------------------------- /dtl/Ses.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | dtl -- Diff Template Library 3 | 4 | In short, Diff Template Library is distributed under so called "BSD license", 5 | 6 | Copyright (c) 2015 Tatsuhiko Kubo 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of the authors nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | /* If you use this library, you must include dtl.hpp only. */ 37 | 38 | #ifndef DTL_SES_H 39 | #define DTL_SES_H 40 | 41 | namespace dtl { 42 | 43 | /** 44 | * Shortest Edit Script template class 45 | */ 46 | template 47 | class Ses : public Sequence< elem > 48 | { 49 | private : 50 | typedef pair< elem, elemInfo > sesElem; 51 | typedef vector< sesElem > sesElemVec; 52 | public : 53 | 54 | Ses () : onlyAdd(true), onlyDelete(true), onlyCopy(true), deletesFirst(false) { 55 | nextDeleteIdx = 0; 56 | } 57 | Ses (bool moveDel) : onlyAdd(true), onlyDelete(true), onlyCopy(true), deletesFirst(moveDel) { 58 | nextDeleteIdx = 0; 59 | } 60 | ~Ses () {} 61 | 62 | bool isOnlyAdd () const { 63 | return onlyAdd; 64 | } 65 | 66 | bool isOnlyDelete () const { 67 | return onlyDelete; 68 | } 69 | 70 | bool isOnlyCopy () const { 71 | return onlyCopy; 72 | } 73 | 74 | bool isOnlyOneOperation () const { 75 | return isOnlyAdd() || isOnlyDelete() || isOnlyCopy(); 76 | } 77 | 78 | bool isChange () const { 79 | return !onlyCopy; 80 | } 81 | 82 | using Sequence< elem >::addSequence; 83 | void addSequence (elem e, long long beforeIdx, long long afterIdx, const edit_t type) { 84 | elemInfo info; 85 | info.beforeIdx = beforeIdx; 86 | info.afterIdx = afterIdx; 87 | info.type = type; 88 | sesElem pe(e, info); 89 | if (!deletesFirst) { 90 | sequence.push_back(pe); 91 | } 92 | switch (type) { 93 | case SES_DELETE: 94 | onlyCopy = false; 95 | onlyAdd = false; 96 | if (deletesFirst) { 97 | sequence.insert(sequence.begin() + nextDeleteIdx, pe); 98 | nextDeleteIdx++; 99 | } 100 | break; 101 | case SES_COMMON: 102 | onlyAdd = false; 103 | onlyDelete = false; 104 | if (deletesFirst) { 105 | sequence.push_back(pe); 106 | nextDeleteIdx = sequence.size(); 107 | } 108 | break; 109 | case SES_ADD: 110 | onlyDelete = false; 111 | onlyCopy = false; 112 | if (deletesFirst) { 113 | sequence.push_back(pe); 114 | } 115 | break; 116 | } 117 | } 118 | 119 | sesElemVec getSequence () const { 120 | return sequence; 121 | } 122 | private : 123 | sesElemVec sequence; 124 | bool onlyAdd; 125 | bool onlyDelete; 126 | bool onlyCopy; 127 | bool deletesFirst; 128 | size_t nextDeleteIdx; 129 | }; 130 | } 131 | 132 | #endif // DTL_SES_H 133 | -------------------------------------------------------------------------------- /dtl/dtl.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | dtl -- Diff Template Library 3 | 4 | In short, Diff Template Library is distributed under so called "BSD license", 5 | 6 | Copyright (c) 2015 Tatsuhiko Kubo 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of the authors nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | #ifndef DTL_H 37 | #define DTL_H 38 | 39 | #include "variables.hpp" 40 | #include "functors.hpp" 41 | #include "Sequence.hpp" 42 | #include "Lcs.hpp" 43 | #include "Ses.hpp" 44 | #include "Diff.hpp" 45 | #include "Diff3.hpp" 46 | 47 | #endif // DTL_H 48 | -------------------------------------------------------------------------------- /dtl/functors.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | dtl -- Diff Template Library 3 | 4 | In short, Diff Template Library is distributed under so called "BSD license", 5 | 6 | Copyright (c) 2015 Tatsuhiko Kubo 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of the authors nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | /* If you use this library, you must include dtl.hpp only. */ 37 | 38 | #ifndef DTL_FUNCTORS_H 39 | #define DTL_FUNCTORS_H 40 | 41 | namespace dtl { 42 | 43 | /** 44 | * printer class template 45 | */ 46 | template 47 | class Printer 48 | { 49 | public : 50 | Printer () : out_(cout) {} 51 | Printer (stream& out) : out_(out) {} 52 | virtual ~Printer () {} 53 | virtual void operator() (const sesElem& se) const = 0; 54 | protected : 55 | stream& out_; 56 | }; 57 | 58 | /** 59 | * common element printer class template 60 | */ 61 | template 62 | class CommonPrinter : public Printer < sesElem, stream > 63 | { 64 | public : 65 | CommonPrinter () : Printer < sesElem, stream > () {} 66 | CommonPrinter (stream& out) : Printer < sesElem, stream > (out) {} 67 | ~CommonPrinter () {} 68 | void operator() (const sesElem& se) const { 69 | this->out_ << SES_MARK_COMMON << se.first << endl; 70 | } 71 | }; 72 | 73 | /** 74 | * ses element printer class template 75 | */ 76 | template 77 | class ChangePrinter : public Printer < sesElem, stream > 78 | { 79 | public : 80 | ChangePrinter () : Printer < sesElem, stream > () {} 81 | ChangePrinter (stream& out) : Printer < sesElem, stream > (out) {} 82 | ~ChangePrinter () {} 83 | void operator() (const sesElem& se) const { 84 | switch (se.second.type) { 85 | case SES_ADD: 86 | this->out_ << SES_MARK_ADD << se.first << endl; 87 | break; 88 | case SES_DELETE: 89 | this->out_ << SES_MARK_DELETE << se.first << endl; 90 | break; 91 | case SES_COMMON: 92 | this->out_ << SES_MARK_COMMON << se.first << endl; 93 | break; 94 | } 95 | } 96 | }; 97 | 98 | /** 99 | * unified format element printer class template 100 | */ 101 | template 102 | class UniHunkPrinter 103 | { 104 | public : 105 | UniHunkPrinter () : out_(cout) {} 106 | UniHunkPrinter (stream& out) : out_(out) {} 107 | ~UniHunkPrinter () {} 108 | void operator() (const uniHunk< sesElem >& hunk) const { 109 | out_ << "@@" 110 | << " -" << hunk.a << "," << hunk.b 111 | << " +" << hunk.c << "," << hunk.d 112 | << " @@" << endl; 113 | 114 | for_each(hunk.common[0].begin(), hunk.common[0].end(), CommonPrinter< sesElem, stream >(out_)); 115 | for_each(hunk.change.begin(), hunk.change.end(), ChangePrinter< sesElem, stream >(out_)); 116 | for_each(hunk.common[1].begin(), hunk.common[1].end(), CommonPrinter< sesElem, stream >(out_)); 117 | } 118 | private : 119 | stream& out_; 120 | }; 121 | 122 | /** 123 | * compare class template 124 | */ 125 | template 126 | class Compare 127 | { 128 | public : 129 | Compare () {} 130 | virtual ~Compare () {} 131 | virtual inline bool impl (const elem& e1, const elem& e2) const { 132 | return e1 == e2; 133 | } 134 | }; 135 | } 136 | 137 | #endif // DTL_FUNCTORS_H 138 | -------------------------------------------------------------------------------- /dtl/variables.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | dtl -- Diff Template Library 3 | 4 | In short, Diff Template Library is distributed under so called "BSD license", 5 | 6 | Copyright (c) 2015 Tatsuhiko Kubo 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of the authors nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | /* If you use this library, you must include dtl.hpp only. */ 37 | 38 | #ifndef DTL_VARIABLES_H 39 | #define DTL_VARIABLES_H 40 | 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | 47 | namespace dtl { 48 | 49 | using std::vector; 50 | using std::string; 51 | using std::pair; 52 | using std::ostream; 53 | using std::list; 54 | using std::for_each; 55 | using std::distance; 56 | using std::fill; 57 | using std::cout; 58 | using std::endl; 59 | using std::rotate; 60 | using std::swap; 61 | using std::max; 62 | 63 | /** 64 | * version string 65 | */ 66 | const string version = "1.19"; 67 | 68 | /** 69 | * type of edit for SES 70 | */ 71 | typedef int edit_t; 72 | const edit_t SES_DELETE = -1; 73 | const edit_t SES_COMMON = 0; 74 | const edit_t SES_ADD = 1; 75 | 76 | /** 77 | * mark of SES 78 | */ 79 | #define SES_MARK_DELETE "-" 80 | #define SES_MARK_COMMON " " 81 | #define SES_MARK_ADD "+" 82 | 83 | /** 84 | * info for Unified Format 85 | */ 86 | typedef struct eleminfo { 87 | long long beforeIdx; // index of prev sequence 88 | long long afterIdx; // index of after sequence 89 | edit_t type; // type of edit(Add, Delete, Common) 90 | bool operator==(const eleminfo& other) const{ 91 | return (this->beforeIdx == other.beforeIdx && this->afterIdx == other.afterIdx && this->type == other.type); 92 | } 93 | } elemInfo; 94 | 95 | const long long DTL_SEPARATE_SIZE = 3; 96 | const long long DTL_CONTEXT_SIZE = 3; 97 | 98 | /** 99 | * cordinate for registering route 100 | */ 101 | typedef struct Point { 102 | long long x; // x cordinate 103 | long long y; // y cordinate 104 | long long k; // vertex 105 | } P; 106 | 107 | /** 108 | * limit of cordinate size 109 | */ 110 | const unsigned long long MAX_CORDINATES_SIZE = 2000000; 111 | 112 | typedef vector< long long > editPath; 113 | typedef vector< P > editPathCordinates; 114 | 115 | /** 116 | * Structure of Unified Format Hunk 117 | */ 118 | template 119 | struct uniHunk { 120 | long long a, b, c, d; // @@ -a,b +c,d @@ 121 | vector< sesElem > common[2]; // anteroposterior commons on changes 122 | vector< sesElem > change; // changes 123 | long long inc_dec_count; // count of increace and decrease 124 | }; 125 | 126 | #define dtl_typedefs(elem, sequence) \ 127 | typedef pair< elem, elemInfo > sesElem; \ 128 | typedef vector< sesElem > sesElemVec; \ 129 | typedef vector< uniHunk< sesElem > > uniHunkVec; \ 130 | typedef list< elem > elemList; \ 131 | typedef vector< elem > elemVec; \ 132 | typedef typename uniHunkVec::iterator uniHunkVec_iter; \ 133 | typedef typename sesElemVec::iterator sesElemVec_iter; \ 134 | typedef typename elemList::iterator elemList_iter; \ 135 | typedef typename sequence::iterator sequence_iter; \ 136 | typedef typename sequence::const_iterator sequence_const_iter; \ 137 | typedef typename elemVec::iterator elemVec_iter; 138 | 139 | 140 | } 141 | 142 | #endif // DTL_VARIABLES_H 143 | -------------------------------------------------------------------------------- /examples/example1/CustomItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.3 3 | 4 | Item { 5 | ColumnLayout { 6 | id: column 7 | anchors.fill: parent 8 | spacing: 0 9 | 10 | Rectangle { 11 | Layout.fillHeight: true 12 | Layout.fillWidth: true 13 | color: "#000000" 14 | } 15 | 16 | Rectangle { 17 | Layout.fillHeight: true 18 | Layout.fillWidth: true 19 | color: "#FF0000" 20 | } 21 | 22 | Rectangle { 23 | Layout.fillHeight: true 24 | Layout.fillWidth: true 25 | color: "#FFCC00" 26 | } 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/example1/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e-fever/snapshottesting/c0fee77447b3ffd373a5a77b25a434ed0490d150/examples/example1/README.md -------------------------------------------------------------------------------- /examples/example1/exmaple1.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | TARGET = tst_example1 3 | CONFIG += warn_on qmltestcase 4 | SOURCES += main.cpp 5 | DEFINES += QUICK_TEST_SOURCE_DIR=\\\"$$PWD/\\\" 6 | 7 | DISTFILES += \ 8 | tst_demo1.qml \ 9 | README.md \ 10 | CustomItem.qml 11 | 12 | include(../../snapshottesting.pri) 13 | 14 | #Run `qpm install` on source's top most folder to obtain required library 15 | include(../../vendor/vendor.pri) 16 | 17 | -------------------------------------------------------------------------------- /examples/example1/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char **argv) 4 | { 5 | return quick_test_main(argc, argv, "Example1", QUICK_TEST_SOURCE_DIR); 6 | } 7 | -------------------------------------------------------------------------------- /examples/example1/tst_demo1.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtTest 1.0 3 | import SnapshotTesting 1.0 4 | 5 | Item { 6 | id: root 7 | width: 640 8 | height: 480 9 | 10 | CustomItem { 11 | // Don't place this under TestCase object. 12 | id: item1 13 | width: 320 14 | height: 180 15 | anchors.centerIn: parent 16 | } 17 | 18 | TestCase { 19 | name: "Demo1" 20 | when: windowShown 21 | 22 | function initTestCase() { 23 | // It is recommended to set snapshotsFile in main.cpp 24 | SnapshotTesting.snapshotsFile = Qt.resolvedUrl("snapshots.json"); 25 | } 26 | 27 | function test_demo1() { 28 | var snapshot = SnapshotTesting.capture(item1); 29 | snapshot = snapshot.replace(Qt.resolvedUrl(".."), ""); 30 | SnapshotTesting.matchStoredSnapshot("test_demo1", snapshot); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /private/snapshottesting_p.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace SnapshotTesting { 9 | 10 | namespace Private { 11 | 12 | class SignalProxy : public QObject { 13 | Q_OBJECT 14 | public: 15 | inline SignalProxy(QObject* parent) : QObject(parent) { 16 | } 17 | signals: 18 | void proxy(); 19 | }; 20 | 21 | class EnumString { 22 | public: 23 | QString componentName; 24 | QString key; 25 | }; 26 | 27 | class QmlType { 28 | public: 29 | inline QmlType() { 30 | isNull = true; 31 | } 32 | QString elementName; 33 | const QMetaObject* meta; 34 | bool isNull; 35 | QString module; 36 | int majorVersion; 37 | int minorVersion; 38 | QString className; 39 | bool isCreatable; 40 | }; 41 | 42 | QString classNameToComponentName(const QString &className); 43 | 44 | /// Obtain the context of the input object which should it be belonged to, but not its parent's scope 45 | /// @deprecated It is replaced by obtainBaseContext 46 | QQmlContext* obtainCurrentScopeContext(QObject* object); 47 | 48 | QQmlContext* obtainCreationContext(QObject* object); 49 | 50 | QList listOwnedContext(QObject* object); 51 | 52 | /// Obtain the "last" context owned by the object. 53 | QQmlContext* obtainBaseContext(QObject* object); 54 | 55 | QString obtainComponentNameByBaseUrl(const QUrl& baseUrl); 56 | 57 | /// Obtain the name of the component by the creation context (the bottom most component) 58 | QString obtainComponentNameByCreationContext(QObject* object); 59 | 60 | QString obtainComponentNameByInheritedContext(QObject * object); 61 | 62 | QString obtainComponentNameByClass(QObject* object); 63 | 64 | QString obtainComponentNameByInheritedClass(QObject* object); 65 | 66 | QString obtainComponentNameByQuickClass(QObject* object); 67 | 68 | QString obtainComponentNameByCurrentScopeContext(QObject* object); 69 | 70 | QString obtainComponentNameOfQtType(QObject* object); 71 | 72 | /// Source component is the object passed to capture function 73 | QString obtainSourceComponentName(QObject* object, bool expandAll = false); 74 | 75 | QStringList listContextUrls(QObject* object); 76 | 77 | QString stringify(QJSEngine*engine, QJSValue value); 78 | 79 | QString stringify(QVariant v); 80 | 81 | QString leftpad(QString text, int pad); 82 | 83 | QString indentText(QString text, int pad); 84 | 85 | QObjectList obtainChildrenObjectList(QObject * object); 86 | 87 | /// Walk on a QML tree structure 88 | void walk(QObject* object, std::function predicate); 89 | 90 | QFuture whenReady(QObject* object); 91 | 92 | /// Wait for a object loaded completely 93 | bool waitUntilReady(QObject* object, int timeout = 10000); 94 | 95 | QString obtainQmlPackage(QObject* object); 96 | 97 | /// Obtain the default values of the object. The result is not hard coded from database. It will create a new default instance and read its default values 98 | QVariantMap obtainDynamicDefaultValues(const QMetaObject* meta); 99 | 100 | QVariantMap obtainDynamicDefaultValues(QObject* object); 101 | 102 | QObject* createQmlComponent(QQmlEngine* engine, QString componentName, QString package, int major, int minor); 103 | 104 | QFuture grabImage(QQuickItem* item); 105 | 106 | /// Convert an image to base64 coding 107 | QByteArray toBase64(const QImage& image); 108 | 109 | QImage combineImages(const QImage& prev, const QImage& curr); 110 | 111 | QString converToPackageNotation(QUrl url); 112 | 113 | QStringList findIgnorePropertyList(QObject* object, QMap classIgnorePropertyList, QMapignoreListForComponent = QMap() ); 114 | 115 | QMap findIgnorePropertyList(QObject* object, const QStringList& rules); 116 | 117 | namespace Rule { 118 | bool isIgnoredProperty(QObject* object, const QString& property, const QStringList& rules); 119 | } 120 | } 121 | } 122 | 123 | Q_DECLARE_METATYPE(SnapshotTesting::Private::EnumString) 124 | -------------------------------------------------------------------------------- /private/snapshottestingoptions.h: -------------------------------------------------------------------------------- 1 | #ifndef SNAPSHOTTESTINGOPTIONS_H 2 | #define SNAPSHOTTESTINGOPTIONS_H 3 | 4 | namespace SnapshotTesting { 5 | 6 | 7 | /// Options for capture 8 | class CaptureOptions { 9 | public: 10 | 11 | inline CaptureOptions() { 12 | captureVisibleItemOnly = true; 13 | expandAll = false; 14 | hideId = false; 15 | indentSize = 4; 16 | captureOnReady = true; 17 | } 18 | 19 | bool captureVisibleItemOnly; 20 | bool expandAll; 21 | bool hideId; 22 | int indentSize; 23 | 24 | /// Capture only if the component is ready (Loader and Image are loaded completely) 25 | bool captureOnReady; 26 | }; 27 | 28 | 29 | } 30 | 31 | #endif // SNAPSHOTTESTINGOPTIONS_H 32 | -------------------------------------------------------------------------------- /private/snapshottestingrenderer.h: -------------------------------------------------------------------------------- 1 | #ifndef SNAPSHOTTESTINGRENDERER_H 2 | #define SNAPSHOTTESTINGRENDERER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace SnapshotTesting { 17 | 18 | /// A off-screen component renderer 19 | 20 | class Renderer { 21 | 22 | public: 23 | Renderer(QQmlEngine* engine); 24 | ~Renderer(); 25 | 26 | QPointer engine() const; 27 | 28 | bool load(const QString& source); 29 | 30 | QFuture whenStill(); 31 | 32 | void waitWhenStill(int timeout = -1); 33 | 34 | QString capture(SnapshotTesting::CaptureOptions options = SnapshotTesting::CaptureOptions()); 35 | 36 | QImage grabScreenshot(); 37 | 38 | /// The loaded item 39 | QObject *item() const; 40 | 41 | private: 42 | QImage render(); 43 | 44 | QPointer m_engine; 45 | 46 | /* Internal variables */ 47 | AsyncFuture::Deferred initialized; 48 | QWindow *owner; 49 | QQuickWindow* window; 50 | QQuickRenderControl* renderControl; 51 | QOffscreenSurface* surface; 52 | QOpenGLContext *context; 53 | QOpenGLFramebufferObject *fbo; 54 | 55 | QObject* m_item; 56 | }; 57 | 58 | } 59 | 60 | #endif // SNAPSHOTTESTINGRENDERER_H 61 | -------------------------------------------------------------------------------- /private/snapshottestingtest.h: -------------------------------------------------------------------------------- 1 | #ifndef SNAPSHOTTESTINGTEST_H 2 | #define SNAPSHOTTESTINGTEST_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "private/snapshottestingoptions.h" 8 | 9 | namespace SnapshotTesting { 10 | 11 | class Test 12 | { 13 | public: 14 | Test(); 15 | 16 | QString name() const; 17 | 18 | void setName(const QString &name); 19 | 20 | QString suffix() const; 21 | 22 | void setSuffix(const QString &suffix); 23 | 24 | QString capture(QObject* object, SnapshotTesting::CaptureOptions options = SnapshotTesting::CaptureOptions()); 25 | 26 | bool match(const QString& snapshot, const QImage screenshot = QImage()); 27 | 28 | private: 29 | 30 | QString m_name; 31 | 32 | QString m_suffix; 33 | }; 34 | 35 | } 36 | 37 | #endif // SNAPSHOTTESTINGTEST_H 38 | -------------------------------------------------------------------------------- /qpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "net.efever.snapshottesting", 3 | "description": "Snapshot Testing", 4 | "author": { 5 | "name": "Ben Lau", 6 | "email": "xbenlau@gmail.com" 7 | }, 8 | "repository": { 9 | "type": "GITHUB", 10 | "url": "git@github.com:e-fever/snapshottesting.git" 11 | }, 12 | "version": { 13 | "label": "0.0.24" 14 | }, 15 | "dependencies": [ 16 | "com.github.benlau.qtshell@0.4.5", 17 | "async.future.pri@0.3.6.5", 18 | "net.efever.aconcurrent@0.1.14" 19 | ], 20 | "license": "APACHE_2_0", 21 | "priFilename": "snapshottesting.pri" 22 | } 23 | -------------------------------------------------------------------------------- /snapshots.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": { 3 | "test_SnapshotTesting_matchStoredSnapshot_Sample1.qml": "Item {\n id: sample1\n x: 10\n y: 10\n width: 640\n height: 480\n Item {\n id: item1\n width: 640\n height: 480\n }\n Item {\n id: item2\n x: 540\n width: 100\n height: 100\n }\n Rectangle {\n id: item3\n width: 100\n height: 100\n }\n Text {\n id: item4\n width: 100\n height: 10\n font.pixelSize: 20\n font.pointSize: -1\n }\n Image {\n id: item5\n width: 100\n height: 100\n implicitHeight: 100\n implicitWidth: 100\n progress: 1\n source: \"sample/snapshot/red-100x100.png\"\n sourceSize: Qt.size(100,100)\n }\n Image {\n id: item6\n }\n Button {\n id: item7\n width: 100\n height: 40\n implicitHeight: 40\n implicitWidth: 100\n }\n}", 4 | "test_SnapshotTesting_matchStoredSnapshot_Sample2.qml": "Item {\n width: 640\n height: 480\n Sample1 {\n x: 10\n y: 10\n width: 640\n height: 480\n Item {\n width: 10\n height: 10\n }\n }\n Sample1 {\n x: 10\n y: 10\n width: 640\n height: 480\n }\n}", 5 | "test_SnapshotTesting_matchStoredSnapshot_Sample3.qml": "Item {\n id: root\n width: 640\n height: 480\n Column {\n id: column\n width: 640\n height: 240\n implicitHeight: 240\n implicitWidth: 640\n Repeater {\n count: 5\n model: 5\n Item {\n id: repeaterItem\n width: 640\n height: 48\n }\n Item {\n id: repeaterItem\n y: 48\n width: 640\n height: 48\n }\n Item {\n id: repeaterItem\n y: 96\n width: 640\n height: 48\n }\n Item {\n id: repeaterItem\n y: 144\n width: 640\n height: 48\n }\n Item {\n id: repeaterItem\n y: 192\n width: 640\n height: 48\n }\n }\n }\n ListView {\n atYEnd: false\n contentHeight: 240\n count: 5\n currentIndex: 0\n model: 5\n Item {\n id: listViewItem\n width: 640\n height: 48\n focus: true\n z: 1\n }\n }\n RowLayout {\n width: 640\n height: 480\n implicitWidth: 5\n Item {\n width: 318\n height: 480\n }\n Item {\n x: 323\n y: 180\n width: 317\n height: 120\n }\n }\n}", 6 | "test_SnapshotTesting_matchStoredSnapshot_Sample4.qml": "Item {\n id: root\n width: 100\n Sample1 {\n id: sampleItem1\n x: 10\n y: 10\n width: 640\n height: 480\n Sample2 {\n id: sampleItem2\n width: 640\n height: 480\n }\n }\n}", 7 | "test_SnapshotTesting_matchStoredSnapshot_expandAll_Sample1.qml": "Item {\n id: sample1\n x: 10\n y: 10\n width: 640\n height: 480\n Item {\n id: item1\n width: 640\n height: 480\n }\n Item {\n id: item2\n x: 540\n width: 100\n height: 100\n }\n Rectangle {\n id: item3\n width: 100\n height: 100\n }\n Text {\n id: item4\n width: 100\n height: 10\n font.pixelSize: 20\n font.pointSize: -1\n }\n Image {\n id: item5\n width: 100\n height: 100\n implicitHeight: 100\n implicitWidth: 100\n progress: 1\n source: \"sample/snapshot/red-100x100.png\"\n sourceSize: Qt.size(100,100)\n }\n Image {\n id: item6\n }\n Button {\n id: item7\n width: 100\n height: 40\n implicitHeight: 40\n implicitWidth: 100\n Rectangle {\n width: 100\n height: 40\n color: \"#e0e0e0\"\n implicitHeight: 40\n implicitWidth: 100\n z: -1\n }\n Text {\n x: 8\n y: 6\n width: 84\n height: 28\n color: \"#353637\"\n effectiveHorizontalAlignment: 4\n elide: 1\n horizontalAlignment: 4\n verticalAlignment: 128\n }\n }\n}", 8 | "test_SnapshotTesting_matchStoredSnapshot_expandAll_Sample2.qml": "Item {\n width: 640\n height: 480\n Item {\n x: 10\n y: 10\n width: 640\n height: 480\n Item {\n id: item1\n width: 640\n height: 480\n }\n Item {\n id: item2\n x: 540\n width: 100\n height: 100\n }\n Rectangle {\n id: item3\n width: 100\n height: 100\n }\n Text {\n id: item4\n width: 100\n height: 10\n font.pixelSize: 20\n font.pointSize: -1\n }\n Image {\n id: item5\n width: 100\n height: 100\n implicitHeight: 100\n implicitWidth: 100\n progress: 1\n source: \"sample/snapshot/red-100x100.png\"\n sourceSize: Qt.size(100,100)\n }\n Image {\n id: item6\n }\n Button {\n id: item7\n width: 100\n height: 40\n implicitHeight: 40\n implicitWidth: 100\n Rectangle {\n width: 100\n height: 40\n color: \"#e0e0e0\"\n implicitHeight: 40\n implicitWidth: 100\n z: -1\n }\n Text {\n x: 8\n y: 6\n width: 84\n height: 28\n color: \"#353637\"\n effectiveHorizontalAlignment: 4\n elide: 1\n horizontalAlignment: 4\n verticalAlignment: 128\n }\n }\n Item {\n width: 10\n height: 10\n }\n }\n Item {\n x: 10\n y: 10\n width: 640\n height: 480\n Item {\n id: item1\n width: 640\n height: 480\n }\n Item {\n id: item2\n x: 540\n width: 100\n height: 100\n }\n Rectangle {\n id: item3\n width: 100\n height: 100\n }\n Text {\n id: item4\n width: 100\n height: 10\n font.pixelSize: 20\n font.pointSize: -1\n }\n Image {\n id: item5\n width: 100\n height: 100\n implicitHeight: 100\n implicitWidth: 100\n progress: 1\n source: \"sample/snapshot/red-100x100.png\"\n sourceSize: Qt.size(100,100)\n }\n Image {\n id: item6\n }\n Button {\n id: item7\n width: 100\n height: 40\n implicitHeight: 40\n implicitWidth: 100\n Rectangle {\n width: 100\n height: 40\n color: \"#e0e0e0\"\n implicitHeight: 40\n implicitWidth: 100\n z: -1\n }\n Text {\n x: 8\n y: 6\n width: 84\n height: 28\n color: \"#353637\"\n effectiveHorizontalAlignment: 4\n elide: 1\n horizontalAlignment: 4\n verticalAlignment: 128\n }\n }\n }\n}", 9 | "test_SnapshotTesting_matchStoredSnapshot_expandAll_Sample3.qml": "Item {\n id: root\n width: 640\n height: 480\n Column {\n id: column\n width: 640\n height: 240\n implicitHeight: 240\n implicitWidth: 640\n Repeater {\n count: 5\n model: 5\n Item {\n id: repeaterItem\n width: 640\n height: 48\n }\n Item {\n id: repeaterItem\n y: 48\n width: 640\n height: 48\n }\n Item {\n id: repeaterItem\n y: 96\n width: 640\n height: 48\n }\n Item {\n id: repeaterItem\n y: 144\n width: 640\n height: 48\n }\n Item {\n id: repeaterItem\n y: 192\n width: 640\n height: 48\n }\n }\n }\n ListView {\n atYEnd: false\n contentHeight: 240\n count: 5\n currentIndex: 0\n model: 5\n Item {\n height: 240\n Item {\n width: 640\n height: 48\n }\n }\n Item {\n id: listViewItem\n width: 640\n height: 48\n focus: true\n z: 1\n }\n Item {\n width: 640\n height: 48\n }\n }\n RowLayout {\n width: 640\n height: 480\n implicitWidth: 5\n Item {\n width: 318\n height: 480\n }\n Item {\n x: 323\n y: 180\n width: 317\n height: 120\n }\n }\n}", 10 | "test_SnapshotTesting_matchStoredSnapshot_expandAll_Sample4.qml": "Item {\n id: root\n width: 100\n Item {\n id: sampleItem1\n x: 10\n y: 10\n width: 640\n height: 480\n Item {\n id: item1\n width: 640\n height: 480\n }\n Item {\n id: item2\n x: 540\n width: 100\n height: 100\n }\n Rectangle {\n id: item3\n width: 100\n height: 100\n }\n Text {\n id: item4\n width: 100\n height: 10\n font.pixelSize: 20\n font.pointSize: -1\n }\n Image {\n id: item5\n width: 100\n height: 100\n implicitHeight: 100\n implicitWidth: 100\n progress: 1\n source: \"sample/snapshot/red-100x100.png\"\n sourceSize: Qt.size(100,100)\n }\n Image {\n id: item6\n }\n Button {\n id: item7\n width: 100\n height: 40\n implicitHeight: 40\n implicitWidth: 100\n Rectangle {\n width: 100\n height: 40\n color: \"#e0e0e0\"\n implicitHeight: 40\n implicitWidth: 100\n z: -1\n }\n Text {\n x: 8\n y: 6\n width: 84\n height: 28\n color: \"#353637\"\n effectiveHorizontalAlignment: 4\n elide: 1\n horizontalAlignment: 4\n verticalAlignment: 128\n }\n }\n Item {\n id: sampleItem2\n width: 640\n height: 480\n Item {\n x: 10\n y: 10\n width: 640\n height: 480\n Item {\n id: item1\n width: 640\n height: 480\n }\n Item {\n id: item2\n x: 540\n width: 100\n height: 100\n }\n Rectangle {\n id: item3\n width: 100\n height: 100\n }\n Text {\n id: item4\n width: 100\n height: 10\n font.pixelSize: 20\n font.pointSize: -1\n }\n Image {\n id: item5\n width: 100\n height: 100\n implicitHeight: 100\n implicitWidth: 100\n progress: 1\n source: \"sample/snapshot/red-100x100.png\"\n sourceSize: Qt.size(100,100)\n }\n Image {\n id: item6\n }\n Button {\n id: item7\n width: 100\n height: 40\n implicitHeight: 40\n implicitWidth: 100\n Rectangle {\n width: 100\n height: 40\n color: \"#e0e0e0\"\n implicitHeight: 40\n implicitWidth: 100\n z: -1\n }\n Text {\n x: 8\n y: 6\n width: 84\n height: 28\n color: \"#353637\"\n effectiveHorizontalAlignment: 4\n elide: 1\n horizontalAlignment: 4\n verticalAlignment: 128\n }\n }\n Item {\n width: 10\n height: 10\n }\n }\n Item {\n x: 10\n y: 10\n width: 640\n height: 480\n Item {\n id: item1\n width: 640\n height: 480\n }\n Item {\n id: item2\n x: 540\n width: 100\n height: 100\n }\n Rectangle {\n id: item3\n width: 100\n height: 100\n }\n Text {\n id: item4\n width: 100\n height: 10\n font.pixelSize: 20\n font.pointSize: -1\n }\n Image {\n id: item5\n width: 100\n height: 100\n implicitHeight: 100\n implicitWidth: 100\n progress: 1\n source: \"sample/snapshot/red-100x100.png\"\n sourceSize: Qt.size(100,100)\n }\n Image {\n id: item6\n }\n Button {\n id: item7\n width: 100\n height: 40\n implicitHeight: 40\n implicitWidth: 100\n Rectangle {\n width: 100\n height: 40\n color: \"#e0e0e0\"\n implicitHeight: 40\n implicitWidth: 100\n z: -1\n }\n Text {\n x: 8\n y: 6\n width: 84\n height: 28\n color: \"#353637\"\n effectiveHorizontalAlignment: 4\n elide: 1\n horizontalAlignment: 4\n verticalAlignment: 128\n }\n }\n }\n }\n }\n}", 11 | "test_SnapshotTesting_matchStoredSnapshot_hideId_Sample1.qml": "Item {\n x: 10\n y: 10\n width: 640\n height: 480\n Item {\n width: 640\n height: 480\n }\n Item {\n x: 540\n width: 100\n height: 100\n }\n Rectangle {\n width: 100\n height: 100\n }\n Text {\n width: 100\n height: 10\n font.pixelSize: 20\n font.pointSize: -1\n }\n Image {\n width: 100\n height: 100\n implicitHeight: 100\n implicitWidth: 100\n progress: 1\n source: \"sample/snapshot/red-100x100.png\"\n sourceSize: Qt.size(100,100)\n }\n Image {\n }\n Button {\n width: 100\n height: 40\n implicitHeight: 40\n implicitWidth: 100\n }\n}", 12 | "test_SnapshotTesting_matchStoredSnapshot_hideId_Sample2.qml": "Item {\n width: 640\n height: 480\n Sample1 {\n x: 10\n y: 10\n width: 640\n height: 480\n Item {\n width: 10\n height: 10\n }\n }\n Sample1 {\n x: 10\n y: 10\n width: 640\n height: 480\n }\n}", 13 | "test_SnapshotTesting_matchStoredSnapshot_hideId_Sample3.qml": "Item {\n width: 640\n height: 480\n Column {\n width: 640\n height: 240\n implicitHeight: 240\n implicitWidth: 640\n Repeater {\n count: 5\n model: 5\n Item {\n width: 640\n height: 48\n }\n Item {\n y: 48\n width: 640\n height: 48\n }\n Item {\n y: 96\n width: 640\n height: 48\n }\n Item {\n y: 144\n width: 640\n height: 48\n }\n Item {\n y: 192\n width: 640\n height: 48\n }\n }\n }\n ListView {\n atYEnd: false\n contentHeight: 240\n count: 5\n currentIndex: 0\n model: 5\n Item {\n width: 640\n height: 48\n focus: true\n z: 1\n }\n }\n RowLayout {\n width: 640\n height: 480\n implicitWidth: 5\n Item {\n width: 318\n height: 480\n }\n Item {\n x: 323\n y: 180\n width: 317\n height: 120\n }\n }\n}", 14 | "test_SnapshotTesting_matchStoredSnapshot_hideId_Sample4.qml": "Item {\n width: 100\n Sample1 {\n x: 10\n y: 10\n width: 640\n height: 480\n Sample2 {\n width: 640\n height: 480\n }\n }\n}", 15 | "test_capture": "Item {\n id: item1\n width: 100\n height: 100\n Item {\n id: child1\n width: 100\n height: 100\n }\n Rectangle {\n width: 100\n height: 100\n color: \"#ff0000\"\n }\n MouseArea {\n id: mouseArea\n width: 100\n height: 100\n }\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /snapshottesting.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace SnapshotTesting { 10 | 11 | Test createTest(); 12 | 13 | /// Set the file name to save the stored snapshots 14 | void setSnapshotsFile(const QString& file); 15 | 16 | /// Get the file name of stored snapshots 17 | QString snapshotsFile(); 18 | 19 | void setScreenshotImagePath(const QString& path); 20 | 21 | /// Load "snapshots" from the "snapshotsFile" 22 | QVariantMap loadStoredSnapshots(); 23 | 24 | /// Run QRegExp replace on everything line of the input 25 | QString replaceLines(const QString& input, QRegExp regexp, QString replace); 26 | 27 | void saveSnapshots(); 28 | 29 | void setSnapshotText(const QString& name , const QString& content); 30 | 31 | void setInteractiveEnabled(bool value); 32 | 33 | bool interactiveEnabled(); 34 | 35 | void setIgnoreAllMismatched(bool value); 36 | 37 | bool ignoreAllMismatched(); 38 | 39 | QString diff(QString original, QString current); 40 | 41 | QString capture(QObject* object, CaptureOptions options = CaptureOptions()); 42 | 43 | bool matchStoredSnapshot(const QString& name, const QString& snapshot); 44 | 45 | bool matchStoredSnapshot(const QString& name, const QString& snapshot, const QImage& screenshot); 46 | 47 | /// Compare the input snapshot and stored snapshot. Returns true if they are matched. Otherwise, it is fale. Unlike matchStoredSnapshot(), it won't trigger UI. 48 | bool tryMatchStoredSnapshot(const QString& name, const QString& snapshot); 49 | 50 | void addSystemIgnoreRule(const QString& rule); 51 | 52 | void removeSystemIgnoreRule(const QString& rule); 53 | 54 | QStringList systemIgnoreRules(); 55 | } 56 | -------------------------------------------------------------------------------- /snapshottesting.pri: -------------------------------------------------------------------------------- 1 | INCLUDEPATH += $$PWD 2 | DEPENDPATH += $$PWD 3 | QML_IMPORT_PATH += $$PWD/qml 4 | 5 | QT += qml-private qml quick 6 | 7 | RESOURCES += \ 8 | $$PWD/snapshottesting.qrc 9 | 10 | HEADERS += \ 11 | $$PWD/snapshottesting.h \ 12 | $$PWD/snapshottestingadapter.h \ 13 | $$PWD/private/snapshottesting_p.h \ 14 | $$PWD/private/snapshottestingoptions.h \ 15 | $$PWD/private/snapshottestingrenderer.h \ 16 | $$PWD/private/snapshottestingtest.h 17 | 18 | SOURCES += \ 19 | $$PWD/snapshottesting.cpp \ 20 | $$PWD/snapshottestingadapter.cpp \ 21 | $$PWD/snapshottestingrenderer.cpp \ 22 | $$PWD/snapshottestingrule.cpp \ 23 | $$PWD/snapshottestingtest.cpp 24 | -------------------------------------------------------------------------------- /snapshottesting.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | 3 | CONFIG += ordered 4 | 5 | SUBDIRS += tests/snapshottestingunittests 6 | -------------------------------------------------------------------------------- /snapshottesting.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | SnapshotTesting/Matcher.qml 4 | SnapshotTesting/MatcherContentView.qml 5 | SnapshotTesting/config/snapshot-config.json 6 | SnapshotTesting/SnapshotTesting.qml 7 | SnapshotTesting/qmldir 8 | SnapshotTesting/ScaleToFitImage.qml 9 | SnapshotTesting/ScreenshotBrowser.qml 10 | 11 | 12 | -------------------------------------------------------------------------------- /snapshottestingadapter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "snapshottesting.h" 5 | #include "snapshottestingadapter.h" 6 | 7 | using namespace SnapshotTesting; 8 | 9 | Adapter::Adapter(QObject *parent) : QObject(parent) 10 | { 11 | 12 | } 13 | 14 | QString Adapter::capture(QObject *object, QVariantMap options) 15 | { 16 | SnapshotTesting::CaptureOptions opt; 17 | 18 | if (options.contains("captureVisibleItemOnly")) { 19 | opt.captureVisibleItemOnly = options["captureVisibleItemOnly"].toBool(); 20 | } 21 | 22 | if (options.contains("expandAll")) { 23 | opt.expandAll = options["expandAll"].toBool(); 24 | } 25 | 26 | if (options.contains("hideId")) { 27 | opt.hideId = options["hideId"].toBool(); 28 | } 29 | if (options.contains("indentSize")) { 30 | opt.indentSize = options["indentSize"].toInt(); 31 | } 32 | 33 | return SnapshotTesting::capture(object, opt); 34 | } 35 | 36 | bool Adapter::matchStoredSnapshot(QString name, QString snapshot) 37 | { 38 | return SnapshotTesting::matchStoredSnapshot(name, snapshot); 39 | } 40 | 41 | QString Adapter::snapshotsFile() const 42 | { 43 | return SnapshotTesting::snapshotsFile(); 44 | } 45 | 46 | void Adapter::setSnapshotsFile(const QString &snapshotFile) 47 | { 48 | SnapshotTesting::setSnapshotsFile(snapshotFile); 49 | emit snapshotFileChanged(); 50 | } 51 | 52 | static QObject *provider(QQmlEngine *engine, QJSEngine *scriptEngine) { 53 | Q_UNUSED(engine); 54 | Q_UNUSED(scriptEngine); 55 | 56 | Adapter* object = new Adapter(); 57 | 58 | return object; 59 | } 60 | 61 | static void registerTypes() { 62 | qmlRegisterSingletonType("SnapshotTesting.Private", 1, 0, "Adapter", provider); 63 | } 64 | 65 | Q_COREAPP_STARTUP_FUNCTION(registerTypes) 66 | -------------------------------------------------------------------------------- /snapshottestingadapter.h: -------------------------------------------------------------------------------- 1 | #ifndef SNAPSHOTTESTINGADAPTER_H 2 | #define SNAPSHOTTESTINGADAPTER_H 3 | 4 | #include 5 | #include 6 | 7 | namespace SnapshotTesting { 8 | 9 | class Adapter : public QObject 10 | { 11 | Q_OBJECT 12 | Q_PROPERTY(QString snapshotsFile READ snapshotsFile WRITE setSnapshotsFile NOTIFY snapshotFileChanged) 13 | public: 14 | explicit Adapter(QObject *parent = nullptr); 15 | 16 | QString snapshotsFile() const; 17 | 18 | void setSnapshotsFile(const QString &snapshotsFile); 19 | 20 | signals: 21 | void snapshotFileChanged(); 22 | 23 | public slots: 24 | QString capture(QObject* object, QVariantMap options = QVariantMap()); 25 | bool matchStoredSnapshot(QString name, QString snapshot); 26 | 27 | }; 28 | 29 | } 30 | 31 | #endif // SNAPSHOTTESTINGADAPTER_H 32 | -------------------------------------------------------------------------------- /snapshottestingrenderer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "snapshottesting.h" 6 | #include "private/snapshottesting_p.h" 7 | 8 | SnapshotTesting::Renderer::Renderer(QQmlEngine* engine) : m_engine(engine) 9 | { 10 | 11 | class RenderControl : public QQuickRenderControl 12 | { 13 | public: 14 | RenderControl(QWindow *w) : m_window(w) { } 15 | QWindow *renderWindow(QPoint *offset) Q_DECL_OVERRIDE { 16 | if (offset) 17 | *offset = QPoint(0, 0); 18 | return m_window; 19 | } 20 | 21 | private: 22 | QWindow *m_window; 23 | }; 24 | 25 | owner = new QWindow(); 26 | renderControl = new RenderControl(owner); 27 | window = new QQuickWindow(renderControl); 28 | 29 | surface = new QOffscreenSurface(); 30 | context = new QOpenGLContext(); 31 | 32 | QSurfaceFormat format; 33 | format.setDepthBufferSize(16); 34 | format.setStencilBufferSize(8); 35 | context->setFormat(format); 36 | context->create(); 37 | 38 | surface->setFormat(format); 39 | surface->create(); 40 | 41 | m_item = nullptr; 42 | fbo = nullptr; 43 | } 44 | 45 | SnapshotTesting::Renderer::~Renderer() 46 | { 47 | context->makeCurrent(surface); 48 | 49 | if (m_item) { 50 | delete m_item; 51 | } 52 | 53 | delete owner; 54 | delete window; 55 | delete renderControl; 56 | 57 | if (fbo) { 58 | delete fbo; 59 | } 60 | 61 | context->doneCurrent(); 62 | 63 | delete surface; 64 | delete context; 65 | } 66 | 67 | bool SnapshotTesting::Renderer::load(const QString &source) 68 | { 69 | // Create the object 70 | auto _create = [=](const QString &source) -> QObject* { 71 | QUrl url = QUrl::fromLocalFile(source); 72 | QQmlComponent component(m_engine.data(), url); 73 | 74 | if (component.isError()) { 75 | const QList errorList = component.errors(); 76 | 77 | for (const QQmlError &error : errorList) { 78 | qWarning() << error.url() << error.line() << error; 79 | } 80 | return 0; 81 | } 82 | 83 | return component.create(); 84 | }; 85 | 86 | auto _init = [=](QObject* rootObject) { 87 | QQuickItem* rootItem = qobject_cast(rootObject); 88 | 89 | if (!rootItem) { 90 | return false; 91 | } 92 | 93 | QObject::connect(window, &QQuickWindow::sceneGraphInitialized, [=]() mutable { 94 | 95 | fbo = new QOpenGLFramebufferObject(window->size(), QOpenGLFramebufferObject::CombinedDepthStencil); 96 | window->setRenderTarget(fbo); 97 | 98 | // This is a dummy render which is needed for some components (e.g ListView) to create their content. 99 | render(); 100 | initialized.complete(); 101 | }); 102 | 103 | auto updateGeom = [=]() { 104 | qreal width = rootItem->width(); 105 | qreal height = rootItem->height(); 106 | if (width == 0.0 || height == 0.0) { 107 | width = 10; 108 | height = 10; 109 | } 110 | window->setGeometry(0,0,width, height); 111 | }; 112 | 113 | rootItem->setParentItem(window->contentItem()); 114 | updateGeom(); 115 | context->makeCurrent(surface); 116 | renderControl->initialize(context); 117 | 118 | QObject::connect(rootItem , &QQuickItem::widthChanged, updateGeom); 119 | QObject::connect(rootItem , &QQuickItem::heightChanged, updateGeom); 120 | 121 | return true; 122 | }; 123 | 124 | QObject* rootObject = _create(source); 125 | rootObject->setParent(owner); 126 | 127 | if (!rootObject) { 128 | return false; 129 | } 130 | 131 | m_item = rootObject; 132 | 133 | QQuickItem* rootItem = qobject_cast(rootObject); 134 | if (!rootItem) { 135 | return true; 136 | } 137 | 138 | _init(rootItem); 139 | return true; 140 | } 141 | 142 | QFuture SnapshotTesting::Renderer::whenStill() 143 | { 144 | return SnapshotTesting::Private::whenReady(m_item); 145 | } 146 | 147 | void SnapshotTesting::Renderer::waitWhenStill(int timeout) 148 | { 149 | QFuture future = whenStill(); 150 | AConcurrent::await(future, timeout); 151 | } 152 | 153 | QString SnapshotTesting::Renderer::capture(SnapshotTesting::CaptureOptions options) 154 | { 155 | if (!m_item) { 156 | return QString(); 157 | } 158 | 159 | return SnapshotTesting::capture(m_item, options); 160 | } 161 | 162 | QImage SnapshotTesting::Renderer::grabScreenshot() 163 | { 164 | QQuickItem* quickItem = qobject_cast(m_item); 165 | 166 | if (!quickItem || quickItem->width() == 0.0 || quickItem->height() == 0.0) { 167 | return QImage(); 168 | } 169 | 170 | auto future = initialized.subscribe([=]() { 171 | return render(); 172 | }).future(); 173 | 174 | AConcurrent::await(future); 175 | 176 | QImage res; 177 | if (!future.isCanceled()) { 178 | res = future.result(); 179 | } 180 | 181 | return res; 182 | } 183 | 184 | QImage SnapshotTesting::Renderer::render() 185 | { 186 | if (!context->makeCurrent(surface)) { 187 | qDebug() << "Failed to render"; 188 | return QImage(); 189 | } 190 | 191 | if (window->width() == 0 || window->height() == 0) { 192 | return QImage(); 193 | } 194 | 195 | renderControl->polishItems(); 196 | renderControl->sync(); 197 | renderControl->render(); 198 | 199 | window->resetOpenGLState(); 200 | 201 | QOpenGLFramebufferObject::bindDefault(); 202 | 203 | context->functions()->glFlush(); 204 | 205 | return fbo->toImage(); 206 | } 207 | 208 | QObject *SnapshotTesting::Renderer::item() const 209 | { 210 | return m_item; 211 | } 212 | 213 | QPointer SnapshotTesting::Renderer::engine() const 214 | { 215 | return m_engine; 216 | } 217 | -------------------------------------------------------------------------------- /snapshottestingrule.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "snapshottesting.h" 3 | 4 | bool SnapshotTesting::Private::Rule::isIgnoredProperty(QObject *object, const QString &property, const QStringList &rules) 5 | { 6 | QMap properties = findIgnorePropertyList(object, rules); 7 | return properties.contains(property); 8 | } 9 | -------------------------------------------------------------------------------- /snapshottestingtest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "snapshottesting.h" 4 | 5 | SnapshotTesting::Test::Test() 6 | { 7 | 8 | } 9 | 10 | QString SnapshotTesting::Test::name() const 11 | { 12 | return m_name; 13 | } 14 | 15 | void SnapshotTesting::Test::setName(const QString &name) 16 | { 17 | m_name = name; 18 | } 19 | 20 | QString SnapshotTesting::Test::suffix() const 21 | { 22 | return m_suffix; 23 | } 24 | 25 | void SnapshotTesting::Test::setSuffix(const QString &suffix) 26 | { 27 | m_suffix = suffix; 28 | } 29 | 30 | QString SnapshotTesting::Test::capture(QObject *object, SnapshotTesting::CaptureOptions options) 31 | { 32 | return SnapshotTesting::capture(object, options); 33 | } 34 | 35 | bool SnapshotTesting::Test::match(const QString &snapshot, const QImage screenshot) 36 | { 37 | QString realName = m_name; 38 | if (!m_suffix.isNull()) { 39 | realName += m_suffix; 40 | } 41 | 42 | return SnapshotTesting::matchStoredSnapshot(realName, snapshot, screenshot); 43 | } 44 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | /.qmake.cache 16 | /.qmake.stash 17 | *.pro.user 18 | *.pro.user.* 19 | *.qbs.user 20 | *.qbs.user.* 21 | *.moc 22 | moc_*.cpp 23 | qrc_*.cpp 24 | ui_*.h 25 | Makefile* 26 | *-build-* 27 | 28 | # QtCreator 29 | 30 | *.autosave 31 | 32 | #QtCtreator Qml 33 | *.qmlproject.user 34 | *.qmlproject.user.* 35 | 36 | build-* 37 | html 38 | build 39 | vendor 40 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/README.md: -------------------------------------------------------------------------------- 1 | 2 | Design Notes 3 | ============ 4 | 5 | Item's implicitWidth and implicitHeight will not be shown by default. Because the objective of SnapshotTesting is to validate the actual geometry of visual items. 6 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "testcases.h" 7 | #include "snapshottesting.h" 8 | #include "testablefunctions.h" 9 | 10 | #if defined(Q_OS_MAC) || defined(Q_OS_LINUX) 11 | #include 12 | #include 13 | #include 14 | void handleBacktrace(int sig) { 15 | void *array[100]; 16 | size_t size; 17 | 18 | // get void*'s for all entries on the stack 19 | size = backtrace(array, 100); 20 | 21 | // print out all the frames to stderr 22 | fprintf(stderr, "Error: signal %d:\n", sig); 23 | backtrace_symbols_fd(array, size, STDERR_FILENO); 24 | exit(1); 25 | } 26 | #endif 27 | 28 | namespace AutoTestRegister { 29 | QUICK_TEST_MAIN(QuickTests) 30 | } 31 | 32 | int main(int argc, char *argv[]) 33 | { 34 | #if defined(Q_OS_MAC) || defined(Q_OS_LINUX) 35 | signal(SIGSEGV, handleBacktrace); 36 | #endif 37 | 38 | qputenv("QML_DISABLE_DISK_CACHE", "1"); 39 | 40 | QGuiApplication app(argc, argv); 41 | 42 | if (Testable::isCI()) { 43 | SnapshotTesting::setInteractiveEnabled(false); 44 | } 45 | 46 | SnapshotTesting::setSnapshotsFile(QtShell::realpath_strip(SRCDIR, "snapshots.json")); 47 | SnapshotTesting::setScreenshotImagePath(QtShell::realpath_strip(QtShell::pwd(), "screenshot")); 48 | 49 | SnapshotTesting::addSystemIgnoreRule("RadioButton@QtQuick.Controls::width"); 50 | SnapshotTesting::addSystemIgnoreRule("RadioButton@QtQuick.Controls::height"); 51 | 52 | TestRunner runner; 53 | runner.addImportPath("qrc:///"); 54 | runner.add(); 55 | runner.add(QString(SRCDIR) + "qmltests"); 56 | 57 | bool error = runner.exec(app.arguments()); 58 | 59 | if (!error) { 60 | qDebug() << "All test cases passed!"; 61 | } 62 | 63 | return error; 64 | } 65 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/packages/QtQuick_Items.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Grid { 4 | columns: 4 5 | 6 | Item { 7 | id: item 8 | width: 100 9 | height: 100 10 | } 11 | 12 | Image { 13 | id: image 14 | source: "../sample/red-100x100.png" 15 | } 16 | 17 | MouseArea { 18 | width: 100 19 | height: 100 20 | } 21 | 22 | Rectangle { 23 | width: 100 24 | height: 100 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/packages/QtQuick_controls_1_2_items.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.2 3 | 4 | Grid { 5 | columns: 4 6 | 7 | Button { 8 | width: 100 9 | height: 100 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/qmltests/tst_SnapshotTesting.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtTest 1.0 3 | import SnapshotTesting 1.0 4 | import "../sample" 5 | 6 | Item { 7 | width: 640 8 | height: 480 9 | 10 | Sample1 { 11 | id: sample1 12 | } 13 | 14 | Sample2 { 15 | id: sample2 16 | } 17 | 18 | Sample5 { 19 | id: sample5 20 | } 21 | 22 | Sample6 { 23 | id: sample6 24 | } 25 | 26 | Sample7 { 27 | id: sample7 28 | } 29 | 30 | Sample8 { 31 | id: sample8 32 | } 33 | 34 | Sample9 { 35 | id: sample9 36 | } 37 | 38 | Sample_QJSValue { 39 | id: sample_qjsvalue 40 | } 41 | 42 | Column { 43 | id: column 44 | } 45 | 46 | TestCase { 47 | name: "SnapshotTesting" 48 | when: windowShown 49 | 50 | function test_capture() { 51 | var objects = { 52 | "sample1": sample1, 53 | "sample2": sample2, 54 | "sample5": sample5, 55 | "sample6": sample6, 56 | "sample7": sample7, 57 | "sample8": sample8, 58 | "sample9": sample9, 59 | "sample_qjsvalue": sample_qjsvalue, 60 | "column": column, 61 | } 62 | 63 | for (var name in objects) { 64 | var target = objects[name]; 65 | var snapshot; 66 | snapshot = SnapshotTesting.capture(target, {indentSize: 4}); 67 | snapshot = snapshot.replace(new RegExp(Qt.resolvedUrl(".."), "g"), ""); 68 | SnapshotTesting.matchStoredSnapshot("qml_test_capture_" + name, snapshot); 69 | 70 | snapshot = SnapshotTesting.capture(target, {expandAll: true}); 71 | snapshot = snapshot.replace(new RegExp(Qt.resolvedUrl(".."), "g"), ""); 72 | SnapshotTesting.matchStoredSnapshot("qml_test_capture_expandAll_" + name, snapshot); 73 | } 74 | } 75 | 76 | function test_caller() { 77 | compare(SnapshotTesting._caller(), "test_caller"); 78 | } 79 | } 80 | 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/qpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | "com.github.benlau.testable@1.0.2.22", 4 | "com.github.benlau.qtshell@0.4.5", 5 | "async.future.pri@0.3.6.5", 6 | "net.efever.aconcurrent@0.1.14", 7 | "com.github.benlau.underline@0.0.2" 8 | ] 9 | } -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/CustomButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 2.0 as QQC2 3 | 4 | QQC2.Button { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample1.qml: -------------------------------------------------------------------------------- 1 | /* Sample1.qml 2 | 3 | 1) Simple structure. No dependence to other component 4 | 2) No list structure like repeater / list view 5 | 3) Collection of basic type 6 | */ 7 | import QtQuick 2.0 8 | import QtQuick.Controls 2.0 as QQC2 9 | 10 | Item { 11 | id: sample1 12 | x: 10 13 | y: 10 14 | width: 640 15 | height: 480 16 | 17 | Item { 18 | id: item1 19 | anchors.fill: parent 20 | } 21 | 22 | Item { 23 | id: item2 24 | width: 100 25 | height: 100 26 | anchors.right: parent.right 27 | } 28 | 29 | Rectangle { 30 | id: item3 31 | width: 100 32 | height: 100 33 | } 34 | 35 | Text { 36 | id: item4 37 | width: 100 38 | height: 10 // It is suggested to assign a fixed height to a text item. Otherwise, the value may be changed on different machine 39 | font.pixelSize: 20 40 | } 41 | 42 | Image { 43 | id: item5 44 | source: "red-100x100.png" 45 | } 46 | 47 | Image { 48 | id: item6 49 | } 50 | 51 | QQC2.Button { 52 | id: item7 53 | } 54 | 55 | MouseArea { 56 | id: item8 57 | acceptedButtons: Qt.RightButton 58 | } 59 | 60 | Canvas { 61 | id: item9 62 | } 63 | 64 | Column { 65 | id: item10 66 | objectName: "Item10"; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample2.qml: -------------------------------------------------------------------------------- 1 | /* Sample2 - Depend on sample1 2 | */ 3 | import QtQuick 2.0 4 | 5 | Item { 6 | width: 640 7 | height: 480 8 | 9 | Sample1 { 10 | id: item_sample1 11 | objectName: "item_sample1" 12 | 13 | Item { 14 | objectName: "InnerItem" 15 | width: 10 16 | height: 10 17 | } 18 | } 19 | 20 | Sample1 { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample3.qml: -------------------------------------------------------------------------------- 1 | /* Sample3 - Test Repeater 2 | */ 3 | import QtQuick 2.0 4 | import QtQuick.Layouts 1.3 5 | 6 | Item { 7 | id: root 8 | width: 640 9 | height: 480 10 | 11 | Column { 12 | id: column 13 | Repeater { 14 | objectName: "Repeater" 15 | model: 5 16 | delegate: Item { 17 | id: repeaterItem 18 | width: 640 19 | height: 48 20 | } 21 | } 22 | } 23 | 24 | ListView { 25 | objectName: "ListView" 26 | model: 5 27 | delegate: Item { 28 | id: listViewItem 29 | width: 640 30 | height: 48 31 | } 32 | } 33 | 34 | RowLayout { 35 | anchors.fill: parent 36 | 37 | Item { 38 | objectName: "InnerItem" 39 | Layout.fillWidth: true 40 | Layout.fillHeight: true 41 | } 42 | 43 | Item { 44 | Layout.fillWidth: true 45 | Layout.fillHeight: true 46 | Layout.maximumHeight: 120 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample4.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Item { 4 | id: root 5 | width: 100 6 | 7 | Sample1 { 8 | id: sampleItem1 9 | 10 | Sample2 { 11 | id: sampleItem2 12 | } 13 | } 14 | 15 | Sample5 { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample5.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.4 2 | 3 | Sample5Form { 4 | } 5 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample5Form.ui.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.4 2 | 3 | Item { 4 | width: 400 5 | height: 400 6 | } 7 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample6.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Sample5 { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample7.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Item { 4 | property int value1: 1 5 | } 6 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample8.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Item { 4 | 5 | Sample7 { 6 | 7 | } 8 | 9 | Sample7 { 10 | property int customValue: 3 11 | } 12 | 13 | Sample9 { 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample9.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | MouseArea { 4 | id: sample9 5 | 6 | property int clickCount: 0 7 | 8 | property rect rect: Qt.rect(12,30, 50,100) 9 | } 10 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample_Control1.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | 4 | Item { 5 | width: 100 6 | height: 100 7 | 8 | RadioButton { 9 | id: radioButton 10 | objectName: "radioButton" 11 | text: "Radio Button" 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample_Layout.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.1 3 | 4 | Grid { 5 | rows: 2 6 | columns: 2 7 | spacing: 10 8 | 9 | Rectangle { 10 | color: "red" 11 | width: 50 12 | height: 60 13 | } 14 | 15 | Rectangle { 16 | color: "blue" 17 | width: 40 18 | height: 70 19 | } 20 | 21 | RowLayout { 22 | width: 200 23 | height: 30 24 | 25 | Rectangle { 26 | color: "green" 27 | Layout.fillWidth: true 28 | Layout.fillHeight: true 29 | } 30 | 31 | Rectangle { 32 | color: "white" 33 | Layout.fillWidth: true 34 | Layout.fillHeight: true 35 | } 36 | 37 | Rectangle { 38 | color: "green" 39 | Layout.fillWidth: true 40 | Layout.fillHeight: true 41 | } 42 | 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample_Loader_Async.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Item { 4 | id: sampleLoaderAsync 5 | 6 | width: image.width 7 | height: image.height 8 | 9 | Loader { 10 | id: loader 11 | 12 | asynchronous: true 13 | 14 | source: Qt.resolvedUrl("./Sample1.qml") 15 | } 16 | 17 | Image { 18 | id: image 19 | 20 | asynchronous: true 21 | cache: false 22 | source: "https://github.com/benlau/junkcode/blob/master/docs/Lanto%20-%20Screenshot%201.png?raw=true" 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/Sample_QJSValue.qml: -------------------------------------------------------------------------------- 1 | // Test QJSValue property 2 | import QtQuick 2.0 3 | 4 | Item { 5 | 6 | property var value1: 1 7 | 8 | property var value2: "2" 9 | 10 | property var value3: ({ 11 | value1: 1, 12 | value2: "2" 13 | }) 14 | 15 | property var value4: [ 1, 2, 3] 16 | } 17 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/sample/red-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e-fever/snapshottesting/c0fee77447b3ffd373a5a77b25a434ed0490d150/tests/snapshottestingunittests/sample/red-100x100.png -------------------------------------------------------------------------------- /tests/snapshottestingunittests/snapshot-default-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "Item@QtQuick": { 3 | "activeFocusOnTab": false, 4 | "antialiasing": false, 5 | "baselineOffset": 0, 6 | "clip": false, 7 | "enabled": true, 8 | "height": 0, 9 | "objectName": "", 10 | "opacity": 1, 11 | "rotation": 0, 12 | "scale": 1, 13 | "smooth": true, 14 | "state": "", 15 | "visible": true, 16 | "width": 0, 17 | "x": 0, 18 | "y": 0, 19 | "z": 0 20 | }, 21 | "MouseArea@QtQuick": { 22 | "acceptedButtons": 1, 23 | "activeFocusOnTab": false, 24 | "antialiasing": false, 25 | "baselineOffset": 0, 26 | "clip": false, 27 | "containsMouse": false, 28 | "containsPress": false, 29 | "cursorShape": 0, 30 | "enabled": true, 31 | "height": 0, 32 | "hoverEnabled": false, 33 | "objectName": "", 34 | "opacity": 1, 35 | "pressAndHoldInterval": 800, 36 | "pressed": false, 37 | "pressedButtons": 0, 38 | "preventStealing": false, 39 | "propagateComposedEvents": false, 40 | "rotation": 0, 41 | "scale": 1, 42 | "scrollGestureEnabled": true, 43 | "smooth": true, 44 | "state": "", 45 | "visible": true, 46 | "width": 0, 47 | "x": 0, 48 | "y": 0, 49 | "z": 0 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/snapshottestingunittests.pro: -------------------------------------------------------------------------------- 1 | QT += testlib qml concurrent 2 | 3 | TARGET = snapshottesting 4 | CONFIG += console 5 | CONFIG -= app_bundle 6 | 7 | TEMPLATE = app 8 | 9 | SOURCES += main.cpp \ 10 | testcases.cpp 11 | 12 | DEFINES += SRCDIR=\\\"$$PWD/\\\" 13 | DEFINES += QUICK_TEST_SOURCE_DIR=\\\"$$PWD/\\\" 14 | 15 | ROOTDIR = $$PWD/../../ 16 | 17 | include(vendor/vendor.pri) 18 | include($$ROOTDIR/snapshottesting.pri) 19 | 20 | DISTFILES += qpm.json \ 21 | ../../appveyor.yml \ 22 | ../../qpm.json \ 23 | README.md \ 24 | packages/QtQuick_Items.qml \ 25 | packages/QtQuick_controls_1_2_items.qml \ 26 | sample/red-100x100.png \ 27 | sample/Sample1.qml \ 28 | sample/Sample2.qml \ 29 | sample/Sample3.qml \ 30 | sample/Sample4.qml \ 31 | qmltests/tst_SnapshotTesting.qml \ 32 | snapshot-default-values.json \ 33 | snapshots.json \ 34 | ../../README.md \ 35 | ../../.travis.yml \ 36 | sample/Sample5Form.ui.qml \ 37 | sample/Sample5.qml \ 38 | sample/Sample6.qml \ 39 | sample/Sample7.qml \ 40 | sample/Sample8.qml \ 41 | sample/Sample9.qml \ 42 | sample/Sample_QJSValue.qml \ 43 | sample/Sample_Loader_Async.qml \ 44 | sample/CustomButton.qml \ 45 | sample/Sample_Layout.qml \ 46 | sample/Sample_Control1.qml 47 | 48 | HEADERS += \ 49 | testcases.h 50 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/testcases.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "automator.h" 15 | #include "testcases.h" 16 | #include "testablefunctions.h" 17 | #include "snapshottesting.h" 18 | #include "private/snapshottesting_p.h" 19 | 20 | using namespace QtShell; 21 | using namespace SnapshotTesting; 22 | using namespace SnapshotTesting::Private; 23 | using namespace SnapshotTesting::Private::Rule; 24 | 25 | Testcases::Testcases(QObject *parent) : QObject(parent) 26 | { 27 | auto ref = [=]() { 28 | QTest::qExec(this, 0, 0); // Autotest detect available test cases of a QObject by looking for "QTest::qExec" in source code 29 | }; 30 | Q_UNUSED(ref); 31 | } 32 | 33 | void Testcases::init() 34 | { 35 | { 36 | // Make sure the QtQuick package is loaded 37 | 38 | QQmlEngine engine; 39 | createQmlComponent(&engine, "Item", "QtQuick", 2 , 0)->deleteLater(); 40 | } 41 | } 42 | 43 | void Testcases::test_obtainQmlPackage() 44 | { 45 | QQuickItem* item = new QQuickItem(); 46 | QString package = obtainQmlPackage(item); 47 | 48 | QCOMPARE(package, QString("QtQuick")); 49 | delete item; 50 | } 51 | 52 | void Testcases::test_obtainDynamicDefaultValues() 53 | { 54 | QQuickItem* item = new QQuickItem(); 55 | 56 | item->setX(100); 57 | QVariantMap defaultValues = obtainDynamicDefaultValues(item); 58 | 59 | QVERIFY(defaultValues.contains("x")); 60 | QCOMPARE(defaultValues["x"].toInt(), 0); 61 | 62 | delete item; 63 | 64 | } 65 | 66 | void Testcases::test_classNameToComponentName() 67 | { 68 | QCOMPARE(classNameToComponentName("AnyOtherClass"), QString("AnyOtherClass")); 69 | QCOMPARE(classNameToComponentName("AnyOtherClass_QML_123"), QString("AnyOtherClass")); 70 | QCOMPARE(classNameToComponentName("QQuickItem"), QString("Item")); 71 | QCOMPARE(classNameToComponentName("QQuickItem_QML_123"), QString("Item")); 72 | QCOMPARE(classNameToComponentName("QQuickItem_QML_4523"), QString("Item")); 73 | QCOMPARE(classNameToComponentName("QQuickText"), QString("Text")); 74 | 75 | { 76 | QQmlEngine engine; 77 | QObject* object = createQmlComponent(&engine, "Canvas", "QtQuick", 2, 0); 78 | 79 | QCOMPARE(classNameToComponentName(object->metaObject()->className()), QString("Canvas")); 80 | object->deleteLater(); 81 | } 82 | } 83 | 84 | void Testcases::test_context() 85 | { 86 | QQmlApplicationEngine engine; 87 | 88 | auto componentNameByBaseContext = [](QObject * object) { 89 | QQmlContext* context = obtainBaseContext(object); 90 | QString res; 91 | if (context) { 92 | res = obtainComponentNameByBaseUrl(context->baseUrl()); 93 | } 94 | 95 | return res; 96 | }; 97 | 98 | { 99 | // Button 100 | QObject* object = createQmlComponent(&engine, "Button", "QtQuick.Controls", 2, 0); 101 | QVERIFY(object); 102 | QCOMPARE(listContextUrls(object).size(), 1); 103 | 104 | QQmlContext* context = qmlContext(object); 105 | QVERIFY(context); 106 | QVERIFY(context->contextObject()); 107 | 108 | object->deleteLater(); 109 | } 110 | 111 | { 112 | QObject* object = createQmlComponent(&engine, "Item", "QtQuick", 2, 0); 113 | QVERIFY(object); 114 | QVERIFY(listContextUrls(object).size() == 0); 115 | object->deleteLater(); 116 | } 117 | 118 | { 119 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample/CustomButton.qml")); 120 | 121 | QQmlComponent component(&engine,url); 122 | 123 | QQuickItem *object = qobject_cast(component.create()); 124 | QVERIFY(object); 125 | QCOMPARE(listContextUrls(object).size(), 2); 126 | 127 | QCOMPARE(obtainComponentNameOfQtType(object), QString("Button")); 128 | qDebug() << "CustomButton" << listContextUrls(object); 129 | 130 | object->deleteLater(); 131 | } 132 | 133 | { 134 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample/Sample1.qml")); 135 | 136 | QQmlComponent component(&engine,url); 137 | 138 | QQuickItem *object = qobject_cast(component.create()); 139 | QVERIFY(object); 140 | QQmlContext* context = qmlContext(object); 141 | 142 | QCOMPARE(obtainComponentNameByBaseUrl(context->baseUrl()), QString("Sample1")); 143 | 144 | context = SnapshotTesting::Private::obtainCreationContext(object); 145 | 146 | QCOMPARE(SnapshotTesting::Private::obtainComponentNameByBaseUrl(context->baseUrl()), QString("Sample1")); 147 | 148 | QCOMPARE(SnapshotTesting::Private::obtainSourceComponentName(object), QString("Item")); 149 | 150 | QVERIFY(obtainCurrentScopeContext(object) == qmlContext(object)); 151 | 152 | { 153 | QQuickItem* item = object->findChild("Item10"); 154 | QVERIFY(item); 155 | 156 | QCOMPARE(SnapshotTesting::Private::obtainSourceComponentName(item), QString("Column")); 157 | } 158 | delete object; 159 | } 160 | 161 | { 162 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample/Sample2.qml")); 163 | 164 | QQmlComponent component(&engine,url); 165 | 166 | QQuickItem *object = qobject_cast(component.create()); 167 | QVERIFY(object); 168 | 169 | { 170 | QObject* innerItem = object->findChild("InnerItem"); 171 | QVERIFY(innerItem); 172 | 173 | QCOMPARE(obtainComponentNameByCurrentScopeContext(innerItem), QString("Item")); 174 | 175 | // The component don't have it's own scope 176 | QCOMPARE(componentNameByBaseContext(innerItem), QString("")); 177 | 178 | } 179 | 180 | { 181 | QObject* item = object->findChild("item_sample1"); 182 | QVERIFY(item); 183 | 184 | QCOMPARE(obtainComponentNameByCurrentScopeContext(item), QString("Sample1")); 185 | 186 | QCOMPARE(componentNameByBaseContext(item), QString("Sample1")); 187 | } 188 | } 189 | 190 | { 191 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample/Sample3.qml")); 192 | 193 | QQmlComponent component(&engine,url); 194 | 195 | QQuickItem *object = qobject_cast(component.create()); 196 | QVERIFY(object); 197 | 198 | { 199 | QObject* item = object->findChild("ListView"); 200 | QVERIFY(item); 201 | 202 | QObjectList list = obtainChildrenObjectList(item); 203 | 204 | QVERIFY(list.size() > 0); 205 | QObject* child = list.first(); 206 | 207 | QCOMPARE(obtainComponentNameByCurrentScopeContext(child), QString("Item")); 208 | } 209 | 210 | { 211 | QObject* item = object->findChild("Repeater"); 212 | QVERIFY(item); 213 | 214 | QQuickItem* child; 215 | QMetaObject::invokeMethod(item,"itemAt",Qt::DirectConnection, 216 | Q_RETURN_ARG(QQuickItem*, child), 217 | Q_ARG(int,0)); 218 | 219 | // obtainComponentNameByCurrentScopeContext can not obtain the current component name in this case 220 | QCOMPARE(obtainComponentNameByCurrentScopeContext(child), QString("Sample3")); // It should be "Item" 221 | 222 | QCOMPARE(componentNameByBaseContext(child), QString("")); 223 | 224 | } 225 | 226 | { 227 | QObject* innerItem = object->findChild("InnerItem"); 228 | QVERIFY(innerItem); 229 | 230 | QCOMPARE(obtainComponentNameByCurrentScopeContext(innerItem), QString("Item")); 231 | } 232 | 233 | } 234 | 235 | 236 | { 237 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample/Sample5.qml")); 238 | 239 | QQmlComponent component(&engine,url); 240 | 241 | QQuickItem *object = qobject_cast(component.create()); 242 | QVERIFY(object); 243 | QQmlContext* context = qmlContext(object); 244 | QCOMPARE(obtainComponentNameByBaseUrl(context->baseUrl()), QString("Sample5")); 245 | 246 | context = SnapshotTesting::Private::obtainCreationContext(object); 247 | QCOMPARE(obtainComponentNameByBaseUrl(context->baseUrl()), QString("Sample5Form")); 248 | 249 | QVERIFY(obtainCurrentScopeContext(object) == qmlContext(object)); 250 | QCOMPARE(SnapshotTesting::Private::obtainSourceComponentName(object), QString("Sample5Form")); 251 | 252 | } 253 | 254 | { 255 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample/Sample2.qml")); 256 | 257 | QQmlComponent component(&engine,url); 258 | 259 | QQuickItem *object = qobject_cast(component.create()); 260 | QVERIFY(object); 261 | 262 | QQuickItem* child = object->findChild("item_sample1"); 263 | 264 | QVERIFY(obtainCurrentScopeContext(child) != qmlContext(child)); 265 | QCOMPARE(obtainSourceComponentName(object), QString("Item")); 266 | } 267 | 268 | { 269 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample/Sample6.qml")); 270 | 271 | QQmlComponent component(&engine,url); 272 | 273 | QQuickItem *object = qobject_cast(component.create()); 274 | QVERIFY(object); 275 | 276 | QCOMPARE(SnapshotTesting::Private::obtainComponentNameByCreationContext(object), QString("Sample5Form")); 277 | 278 | QCOMPARE(obtainComponentNameByBaseUrl(obtainCurrentScopeContext(object)->baseUrl()), QString("Sample6")); 279 | 280 | QCOMPARE(SnapshotTesting::Private::obtainSourceComponentName(object), QString("Sample5")); 281 | } 282 | 283 | { 284 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample/Sample7.qml")); 285 | 286 | QQmlComponent component(&engine,url); 287 | 288 | QQuickItem *object = qobject_cast(component.create()); 289 | QVERIFY(object); 290 | 291 | QCOMPARE(obtainComponentNameByClass(object), QString("Sample7")); 292 | 293 | QCOMPARE(SnapshotTesting::Private::obtainComponentNameOfQtType(object), QString("Item")); 294 | QCOMPARE(SnapshotTesting::Private::obtainSourceComponentName(object), QString("Item")); 295 | QCOMPARE(SnapshotTesting::Private::obtainSourceComponentName(object, true), QString("Item")); 296 | 297 | delete object; 298 | } 299 | 300 | { 301 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample/Sample_Control1.qml")); 302 | 303 | QQmlComponent component(&engine,url); 304 | 305 | QQuickItem *object = qobject_cast(component.create()); 306 | QVERIFY(object); 307 | 308 | { 309 | QQuickItem *radioButton = qobject_cast(object->findChild("radioButton")); 310 | QVERIFY(radioButton); 311 | 312 | QCOMPARE(componentNameByBaseContext(radioButton), QString("RadioButton")); 313 | QStringList rules; 314 | rules << "RadioButton@QtQuick.Controls::implicitHeight"; 315 | auto properties = findIgnorePropertyList(radioButton, rules); 316 | QCOMPARE(properties.size(), 1); 317 | } 318 | } 319 | 320 | { 321 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample/Sample_Layout.qml")); 322 | 323 | QQmlComponent component(&engine,url); 324 | 325 | QQuickItem *object = qobject_cast(component.create()); 326 | QVERIFY(object); 327 | 328 | QCOMPARE(obtainComponentNameOfQtType(object), QString("Grid")); 329 | 330 | } 331 | } 332 | 333 | void Testcases::test_loading_config() 334 | { 335 | { 336 | QString text = QtShell::cat(":/qt-project.org/imports/SnapshotTesting/config/snapshot-config.json"); 337 | 338 | QJsonParseError error; 339 | QJsonDocument doc = QJsonDocument::fromJson(text.toUtf8(),&error); 340 | Q_UNUSED(doc); 341 | 342 | QVERIFY(error.error == QJsonParseError::NoError); 343 | } 344 | } 345 | 346 | void Testcases::test_grabImage() 347 | { 348 | if (Testable::isCI()) { 349 | qDebug() << "Skip this test in CI environment"; 350 | return; 351 | } 352 | 353 | QQuickWindow window; 354 | QQmlEngine engine; 355 | QObject* object = createQmlComponent(&engine, "Item", "QtQuick", 2, 0); 356 | 357 | QQuickItem *item = qobject_cast(object); 358 | QVERIFY(item); 359 | item->setWidth(200); 360 | item->setHeight(300); 361 | 362 | item->setParentItem(window.contentItem()); 363 | window.show(); 364 | 365 | Automator::wait(2000); 366 | 367 | QFuture future = grabImage(item); 368 | 369 | AConcurrent::await(future); 370 | 371 | QImage image = future.result(); 372 | 373 | QCOMPARE(image.size(), QSize(200,300)); 374 | } 375 | 376 | void Testcases::test_ScreenshotBrowser() 377 | { 378 | QImage image1(QSize(320,240), QImage::Format_RGB32); 379 | image1.fill(QColor(255,0,0)); 380 | 381 | QImage image2(QSize(160,240), QImage::Format_RGB32); 382 | image2.fill(QColor(0,0,255)); 383 | 384 | QString function = QTest::currentTestFunction(); 385 | 386 | QByteArray base64 = SnapshotTesting::Private::toBase64(image1); 387 | 388 | QQmlApplicationEngine engine; 389 | 390 | QQuickWindow window; 391 | 392 | QQmlComponent component(&engine, QUrl("qrc:///qt-project.org/imports/SnapshotTesting/ScreenshotBrowser.qml")); 393 | 394 | QQuickItem* item = qobject_cast(component.create()); 395 | QVERIFY(item); 396 | 397 | item->setSize(QSize(640,480)); 398 | item->setParentItem(window.contentItem()); 399 | item->setProperty("screenshot", base64); 400 | 401 | window.resize(QSize(item->width(), item->height())); 402 | window.show(); 403 | 404 | auto replace = [=](const QString& source) { 405 | QString t = source; 406 | t = t.replace(QRegExp("source: \"[a-zA-Z0-9=+;:,/]*\""),""); 407 | t = t.replace(QRegExp("screenshot: \"[a-zA-Z0-9=+;:,/]*\""),""); 408 | t = t.replace(QRegExp("[a-z]*Screenshot: \"[a-zA-Z0-9=+;:,/]*\""),""); 409 | return t; 410 | }; 411 | 412 | Automator::wait(100); 413 | 414 | SnapshotTesting::CaptureOptions options; 415 | options.expandAll = true; 416 | 417 | { 418 | QString snapshot = SnapshotTesting::capture(item, options); 419 | snapshot = replace(snapshot); 420 | 421 | QVERIFY(SnapshotTesting::matchStoredSnapshot(function + "_Single", snapshot)); 422 | } 423 | 424 | { 425 | item->setProperty("previousScreenshot", SnapshotTesting::Private::toBase64(image2)); 426 | Automator::wait(100); 427 | 428 | QString snapshot = SnapshotTesting::capture(item, options); 429 | snapshot = replace(snapshot); 430 | 431 | QVERIFY(SnapshotTesting::matchStoredSnapshot(function + "_Dual", snapshot)); 432 | } 433 | 434 | { 435 | QImage combined = SnapshotTesting::Private::combineImages(image1, image2); 436 | 437 | item->setProperty("combinedScreenshot", SnapshotTesting::Private::toBase64(combined)); 438 | QMetaObject::invokeMethod(item, "showCombinedScreenshot"); 439 | 440 | Automator automator(&engine); 441 | Automator::wait(10); 442 | 443 | QString snapshot = SnapshotTesting::capture(item, options); 444 | snapshot = replace(snapshot); 445 | 446 | QVERIFY(SnapshotTesting::matchStoredSnapshot(function + "_Combined", snapshot)); 447 | 448 | } 449 | 450 | 451 | 452 | delete item; 453 | } 454 | 455 | void Testcases::test_convertToPackageNotation() 456 | { 457 | { 458 | QUrl url("qrc:///qt-project.org/imports/SnapshotTesting/ScreenshotBrowser.qml"); 459 | 460 | QCOMPARE(converToPackageNotation(url), QString("qt-project.org.imports.SnapshotTesting")); 461 | } 462 | 463 | { 464 | QUrl url("qrc:///qt-project.org/imports/SnapshotTesting.2/ScreenshotBrowser.qml"); 465 | 466 | QCOMPARE(converToPackageNotation(url), QString("qt-project.org.imports.SnapshotTesting")); 467 | } 468 | 469 | } 470 | 471 | void Testcases::test_replaceLines() 472 | { 473 | QString input = "123\n456\n789"; 474 | QString expectedOutput = "123\n\n789"; 475 | 476 | QCOMPARE(SnapshotTesting::replaceLines(input, QRegExp(".*5.*"),""), expectedOutput); 477 | } 478 | 479 | void Testcases::test_qml_loading() 480 | { 481 | QFETCH(QString, input); 482 | 483 | QQmlEngine engine; 484 | engine.addImportPath("qrc:///"); 485 | 486 | QQmlComponent comp(&engine); 487 | QString sourceCode = QtShell::cat(input); 488 | auto url = QUrl::fromLocalFile(input); 489 | 490 | comp.setData(sourceCode.toUtf8(), url); 491 | 492 | if (comp.isError()) { 493 | qDebug() << QString("%1 : Load Failed. Reason : %2").arg(input).arg(comp.errorString()); 494 | } 495 | QVERIFY(!comp.isError()); 496 | } 497 | 498 | void Testcases::test_qml_loading_data() 499 | { 500 | QTest::addColumn("input"); 501 | QStringList files; 502 | files << QtShell::find(QtShell::realpath_strip(SRCDIR,"../../SnapshotTesting"), "*.qml"); 503 | 504 | foreach (QString file , files) { 505 | QString content = QtShell::cat(file); 506 | content = content.toLower(); 507 | 508 | if (content.indexOf("pragma singleton") != -1) { 509 | continue; 510 | } 511 | 512 | QTest::newRow(QtShell::basename(file).toLocal8Bit().constData()) << file; 513 | } 514 | } 515 | 516 | void Testcases::test_isIgnoredProperty() 517 | { 518 | { 519 | QStringList rules; 520 | rules << "QQuickItem::parent"; 521 | 522 | // Test ignore class 523 | QQuickItem* item = new QQuickItem(); 524 | 525 | QCOMPARE(isIgnoredProperty(item, "parent", rules), true); 526 | QCOMPARE(isIgnoredProperty(item, "width", rules), false); 527 | QCOMPARE(isIgnoredProperty(item, "height", rules), false); 528 | 529 | delete item; 530 | } 531 | 532 | { 533 | QStringList rules; 534 | rules << "QQuickItem::parent" 535 | << "Sample9@sample::containsMouse" 536 | << "MouseArea@QtQuick::height" 537 | << "#TestItem::width"; 538 | 539 | QQmlApplicationEngine engine; 540 | 541 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample", "Sample9.qml")); 542 | 543 | QQmlComponent component(&engine, url); 544 | 545 | QQuickItem* sample9 = qobject_cast(component.create()); 546 | 547 | // Validate 548 | 549 | QCOMPARE(isIgnoredProperty(sample9, "parent", rules), true); 550 | QCOMPARE(isIgnoredProperty(sample9, "width", rules), false); 551 | QCOMPARE(isIgnoredProperty(sample9, "containsMouse", rules), true); 552 | QCOMPARE(isIgnoredProperty(sample9, "height", rules), true); 553 | 554 | // Validate "#TestItem::width" 555 | 556 | sample9->setObjectName("TestItem"); 557 | QCOMPARE(isIgnoredProperty(sample9, "parent", rules), true); 558 | QCOMPARE(isIgnoredProperty(sample9, "width", rules), true); 559 | QCOMPARE(isIgnoredProperty(sample9, "containsMouse", rules), true); 560 | 561 | delete sample9; 562 | } 563 | 564 | { 565 | QStringList rules; 566 | rules << "Text@QtQuick::baseUrl"; 567 | 568 | QQmlApplicationEngine engine; 569 | 570 | QUrl url = QUrl::fromLocalFile(QtShell::realpath_strip(SRCDIR, "sample", "Sample1.qml")); 571 | 572 | QQmlComponent component(&engine, url); 573 | 574 | QQuickItem* sample1 = qobject_cast(component.create()); 575 | QVERIFY(sample1); 576 | auto items = sample1->childItems(); 577 | QQuickItem* text = nullptr; 578 | 579 | for (auto item: items) { 580 | if (QString(item->metaObject()->className()) == "QQuickText") { 581 | text = item; 582 | break; 583 | } 584 | } 585 | 586 | QVERIFY(text); 587 | QCOMPARE(isIgnoredProperty(text, "baseUrl", rules), true); 588 | } 589 | 590 | auto testButton_1_2 = [=]() { 591 | QQmlApplicationEngine engine; 592 | 593 | QObject* object = createQmlComponent(&engine, "Button", "QtQuick.Controls", 1, 2); 594 | 595 | auto rules = QStringList{"Button@QtQuick.Controls::__effectivePressed"}; 596 | 597 | QCOMPARE(isIgnoredProperty(object, "__effectivePressed", rules), true); 598 | 599 | object->deleteLater(); 600 | }; 601 | 602 | testButton_1_2(); 603 | } 604 | 605 | void Testcases::createDefaultValuesConfig() 606 | { 607 | /* @TODO - Migration Plan 608 | * 1. Drop snapshot-config.json 609 | * 2. Replace by snapshot-default-values.json to get hard coded default values 610 | */ 611 | 612 | //@TODO - Migrate to the QRC 613 | QString jsonFile = QtShell::realpath_strip(SRCDIR, "snapshot-default-values.json"); 614 | QString text = QtShell::cat(jsonFile); 615 | 616 | auto defaultValuesConfig = _::parse(text); 617 | 618 | auto appendDefaultValue = [&](QString component, QString package, int major, int minor) { 619 | QQmlEngine engine; 620 | QObject* object = createQmlComponent(&engine, component, package, major, minor); 621 | 622 | QVariantMap properties; 623 | 624 | auto ignoreRules = SnapshotTesting::systemIgnoreRules(); 625 | auto ignoreProperties = SnapshotTesting::Private::findIgnorePropertyList(object, ignoreRules).keys(); 626 | 627 | _::merge(properties, object); 628 | properties = _::omit(properties, ignoreProperties); 629 | 630 | QString name = QString("%1@%2").arg(component).arg(package); 631 | 632 | defaultValuesConfig[name] = properties; 633 | }; 634 | 635 | appendDefaultValue("Item", "QtQuick", 2, 0); 636 | appendDefaultValue("MouseArea", "QtQuick", 2, 0); 637 | // appendDefaultValue("Button", "QtQuick.Controls", 1, 2); 638 | 639 | QFile file(jsonFile); 640 | QVERIFY(file.open(QIODevice::WriteOnly)); 641 | 642 | file.write(_::stringify(defaultValuesConfig, true).toUtf8()); 643 | file.close(); 644 | } 645 | 646 | void Testcases::test_SnapshotTesting_diff() 647 | { 648 | QString text1 = "A\nB\nC"; 649 | QString text2 = "A\nD\nC"; 650 | 651 | QString result = SnapshotTesting::diff(text1, text2); 652 | 653 | qDebug().noquote() << result; 654 | } 655 | 656 | void Testcases::test_SnapshotTesting_saveSnapshots() 657 | { 658 | SnapshotTesting::saveSnapshots(); 659 | } 660 | 661 | void Testcases::test_SnapshotTesting_addSystemIgnoreRule() 662 | { 663 | QString input = QtShell::realpath_strip(SRCDIR, "sample/Sample1.qml"); 664 | 665 | QQmlApplicationEngine engine; 666 | QUrl url = QUrl::fromLocalFile(input); 667 | 668 | QQmlComponent component(&engine,url); 669 | QQuickItem *childItem = qobject_cast(component.create()); 670 | QVERIFY(childItem); 671 | 672 | QString name, text; 673 | 674 | name = QString("%1_default").arg(QTest::currentTestFunction()); 675 | 676 | text = SnapshotTesting::capture(childItem); 677 | text.replace(QUrl::fromLocalFile(QString(SRCDIR)).toString(), ""); 678 | QVERIFY(SnapshotTesting::matchStoredSnapshot(name, text)); 679 | 680 | SnapshotTesting::addSystemIgnoreRule("QQuickRectangle::width"); 681 | name = QString("%1_set").arg(QTest::currentTestFunction()); 682 | 683 | text = SnapshotTesting::capture(childItem); 684 | text.replace(QUrl::fromLocalFile(QString(SRCDIR)).toString(), ""); 685 | QVERIFY(SnapshotTesting::matchStoredSnapshot(name, text)); 686 | 687 | SnapshotTesting::removeSystemIgnoreRule("QQuickRectangle::width"); 688 | name = QString("%1_default").arg(QTest::currentTestFunction()); 689 | 690 | text = SnapshotTesting::capture(childItem); 691 | text.replace(QUrl::fromLocalFile(QString(SRCDIR)).toString(), ""); 692 | QVERIFY(SnapshotTesting::matchStoredSnapshot(name, text)); 693 | } 694 | 695 | void Testcases::test_SnapshotTesting_capture_QObject() 696 | { 697 | QObject object; 698 | 699 | QString snapshot = SnapshotTesting::capture(&object); 700 | 701 | QCOMPARE(snapshot, QString("")); 702 | } 703 | 704 | void Testcases::test_SnapshotTesting_capture_RadioButton() 705 | { 706 | QQmlEngine engine; 707 | QString function = QTest::currentTestFunction(); 708 | 709 | 710 | { 711 | QString qml = "import QtQuick 2.0; import QtQuick.Controls 1.1; Item { RadioButton {} }"; 712 | 713 | QQmlComponent comp (&engine); 714 | comp.setData(qml.toUtf8(),QUrl()); 715 | QObject* ret = comp.create(); 716 | 717 | QQuickItem* item = qobject_cast(ret); 718 | 719 | QVERIFY(item); 720 | 721 | SnapshotTesting::CaptureOptions options; 722 | options.expandAll = true; 723 | QString snapshot = SnapshotTesting::capture(item, options); 724 | qDebug() << snapshot; 725 | QVERIFY(SnapshotTesting::matchStoredSnapshot(function + "_Normal", snapshot)); 726 | 727 | item->deleteLater(); 728 | } 729 | 730 | { 731 | 732 | QObject* object = createQmlComponent(&engine, "RadioButton", "QtQuick.Controls",1,1); 733 | QQuickItem* item = qobject_cast(object); 734 | QVERIFY(item); 735 | 736 | qDebug() << item << item->metaObject()->className(); 737 | 738 | qDebug() << item->metaObject()->superClass()->superClass()->className(); 739 | 740 | SnapshotTesting::CaptureOptions options; 741 | options.expandAll = true; 742 | QString snapshot = SnapshotTesting::capture(item, options); 743 | QVERIFY(SnapshotTesting::matchStoredSnapshot(function + "_Single", snapshot)); 744 | 745 | item->deleteLater(); 746 | 747 | } 748 | 749 | 750 | } 751 | 752 | void Testcases::test_SnapshotTesting_matchStoredSnapshot() 753 | { 754 | QFETCH(QString, input); 755 | 756 | QString fileName = QtShell::basename(input); 757 | 758 | QQmlApplicationEngine engine; 759 | QUrl url = QUrl::fromLocalFile(input); 760 | 761 | QQmlComponent component(&engine,url); 762 | QQuickItem *childItem = qobject_cast(component.create()); 763 | QVERIFY(childItem); 764 | 765 | QString name = QString("%1_%2").arg(QTest::currentTestFunction()).arg(fileName); 766 | 767 | QString text = SnapshotTesting::capture(childItem); 768 | text.replace(QUrl::fromLocalFile(QString(SRCDIR)).toString(), ""); 769 | text.replace(QString(SRCDIR), ""); 770 | 771 | QVERIFY(SnapshotTesting::matchStoredSnapshot(name, text)); 772 | } 773 | 774 | void Testcases::test_SnapshotTesting_matchStoredSnapshot_data() 775 | { 776 | scanQmlFiles(); 777 | } 778 | 779 | void Testcases::test_SnapshotTesting_matchStoredSnapshot_expandAll() 780 | { 781 | QFETCH(QString, input); 782 | 783 | QString fileName = QtShell::basename(input); 784 | 785 | QQmlApplicationEngine engine; 786 | 787 | QUrl url = QUrl::fromLocalFile(input); 788 | 789 | QQmlComponent component(&engine,url); 790 | QQuickItem *childItem = qobject_cast(component.create()); 791 | QVERIFY(childItem); 792 | 793 | SnapshotTesting::CaptureOptions options; 794 | options.expandAll = true; 795 | QString name = QString("%1_%2").arg(QTest::currentTestFunction()).arg(fileName); 796 | 797 | QString text = SnapshotTesting::capture(childItem, options); 798 | text.replace(QUrl::fromLocalFile(QString(SRCDIR)).toString(), ""); 799 | text.replace(QString(SRCDIR), ""); 800 | 801 | QVERIFY(SnapshotTesting::matchStoredSnapshot(name, text)); 802 | } 803 | 804 | void Testcases::test_SnapshotTesting_matchStoredSnapshot_expandAll_data() 805 | { 806 | scanQmlFiles(); 807 | } 808 | 809 | void Testcases::test_SnapshotTesting_matchStoredSnapshot_hideId() 810 | { 811 | QFETCH(QString, input); 812 | 813 | QString fileName = QtShell::basename(input); 814 | 815 | QQmlApplicationEngine engine; 816 | 817 | QUrl url = QUrl::fromLocalFile(input); 818 | 819 | QQmlComponent component(&engine,url); 820 | QQuickItem *childItem = qobject_cast(component.create()); 821 | QVERIFY(childItem); 822 | 823 | SnapshotTesting::CaptureOptions options; 824 | options.hideId = true; 825 | QString name = QString("%1_%2").arg(QTest::currentTestFunction()).arg(fileName); 826 | 827 | QString text = SnapshotTesting::capture(childItem, options); 828 | text.replace(QUrl::fromLocalFile(QString(SRCDIR)).toString(), ""); 829 | text.replace(QString(SRCDIR), ""); 830 | 831 | QVERIFY(SnapshotTesting::matchStoredSnapshot(name, text)); 832 | } 833 | 834 | void Testcases::test_SnapshotTesting_matchStoredSnapshot_hideId_data() 835 | { 836 | scanQmlFiles(); 837 | } 838 | 839 | void Testcases::test_SnapshotTesting_matchStoredSnapshot_screenshot() 840 | { 841 | QFETCH(QString, input); 842 | 843 | QString fileName = QtShell::basename(input); 844 | QString name = QString("%1_%2").arg(QTest::currentTestFunction()).arg(fileName); 845 | 846 | QQmlEngine engine; 847 | SnapshotTesting::Renderer renderer(&engine); 848 | 849 | SnapshotTesting::CaptureOptions options; 850 | options.hideId = true; 851 | 852 | QVERIFY(renderer.load(input)); 853 | 854 | renderer.waitWhenStill(1000); 855 | 856 | QString snapshot = renderer.capture(options); 857 | QImage screenshot = renderer.grabScreenshot();; 858 | 859 | snapshot.replace(QUrl::fromLocalFile(QString(SRCDIR)).toString(), ""); 860 | 861 | QVERIFY(SnapshotTesting::matchStoredSnapshot(name, snapshot, screenshot)); 862 | } 863 | 864 | void Testcases::test_SnapshotTesting_matchStoredSnapshot_screenshot_data() 865 | { 866 | scanQmlFiles(); 867 | } 868 | 869 | void Testcases::test_SnapshotTesting_createTest() 870 | { 871 | QFETCH(QString, input); 872 | 873 | QString fileName = QtShell::basename(input); 874 | 875 | QQmlEngine engine; 876 | Renderer renderer(&engine); 877 | QVERIFY(renderer.load(input)); 878 | 879 | auto test = SnapshotTesting::createTest(); 880 | 881 | QCOMPARE(test.name(), QString(QTest::currentTestFunction())); 882 | 883 | test.setSuffix(QString("_") + fileName); 884 | 885 | QString snapshot = test.capture(renderer.item()); 886 | snapshot.replace(QUrl::fromLocalFile(QString(SRCDIR)).toString(), ""); 887 | snapshot.replace(QString(SRCDIR), ""); 888 | 889 | QImage screenshot = renderer.grabScreenshot(); 890 | 891 | QVERIFY(test.match(snapshot, screenshot)); 892 | } 893 | 894 | void Testcases::test_SnapshotTesting_createTest_data() 895 | { 896 | scanQmlFiles(); 897 | } 898 | 899 | void Testcases::scanQmlFiles() 900 | { 901 | QTest::addColumn("input"); 902 | 903 | QStringList files = QtShell::find(QtShell::realpath_strip(SRCDIR, "sample"), "*.qml"); 904 | 905 | files.append(QtShell::find(QtShell::realpath_strip(SRCDIR, "packages"), "*.qml")); 906 | 907 | foreach (QString file, files) { 908 | QTest::newRow(file.toUtf8().constData()) << file; 909 | } 910 | } 911 | ; 912 | -------------------------------------------------------------------------------- /tests/snapshottestingunittests/testcases.h: -------------------------------------------------------------------------------- 1 | #ifndef SNAPSHOTTESTS_H 2 | #define SNAPSHOTTESTS_H 3 | 4 | #include 5 | 6 | class Testcases : public QObject 7 | { 8 | Q_OBJECT 9 | public: 10 | explicit Testcases(QObject *parent = nullptr); 11 | 12 | signals: 13 | 14 | public slots: 15 | 16 | private slots: 17 | /* Private API */ 18 | 19 | void init(); 20 | 21 | void test_obtainQmlPackage(); 22 | 23 | void test_obtainDynamicDefaultValues(); 24 | 25 | void test_classNameToComponentName(); 26 | 27 | void test_context(); 28 | 29 | void test_loading_config(); 30 | 31 | void test_grabImage(); 32 | 33 | void test_ScreenshotBrowser(); 34 | 35 | void test_convertToPackageNotation(); 36 | 37 | void test_replaceLines(); 38 | 39 | void test_qml_loading(); 40 | void test_qml_loading_data(); 41 | 42 | void test_isIgnoredProperty(); 43 | 44 | /* Data Generator */ 45 | 46 | void createDefaultValuesConfig(); 47 | 48 | /* Public API */ 49 | 50 | void test_SnapshotTesting_diff(); 51 | 52 | void test_SnapshotTesting_saveSnapshots(); 53 | 54 | void test_SnapshotTesting_addSystemIgnoreRule(); 55 | 56 | void test_SnapshotTesting_capture_QObject(); 57 | 58 | void test_SnapshotTesting_capture_RadioButton(); 59 | 60 | void test_SnapshotTesting_matchStoredSnapshot(); 61 | void test_SnapshotTesting_matchStoredSnapshot_data(); 62 | 63 | void test_SnapshotTesting_matchStoredSnapshot_expandAll(); 64 | void test_SnapshotTesting_matchStoredSnapshot_expandAll_data(); 65 | 66 | void test_SnapshotTesting_matchStoredSnapshot_hideId(); 67 | void test_SnapshotTesting_matchStoredSnapshot_hideId_data(); 68 | 69 | void test_SnapshotTesting_matchStoredSnapshot_screenshot(); 70 | void test_SnapshotTesting_matchStoredSnapshot_screenshot_data(); 71 | 72 | void test_SnapshotTesting_createTest(); 73 | void test_SnapshotTesting_createTest_data(); 74 | 75 | private: 76 | void scanQmlFiles(); 77 | }; 78 | 79 | #endif // SNAPSHOTTESTS_H 80 | --------------------------------------------------------------------------------