Remove submodules

This commit is contained in:
Alex Spataru 2024-09-18 22:20:55 -05:00
parent 8e1852459d
commit 2bb8290a0a
89 changed files with 7581 additions and 2763 deletions

View File

@ -58,7 +58,7 @@ jobs:
- name: '⚙️ Install dependencies'
run: |
sudo apt-get update
sudo apt-get install libgl1-mesa-dev libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libzstd-dev libxcb-image0-dev libxcb-util0-dev libxcb-cursor-dev libssl-dev libudev-dev libusb-1.0-0-dev
sudo apt-get install libgl1-mesa-dev libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libzstd-dev libxcb-image0-dev libxcb-util0-dev libxcb-cursor-dev libssl-dev libudev-dev libusb-1.0-0-dev libssl-dev
- name: '⚙️ Install CMake'
uses: lukka/get-cmake@latest

12
.gitmodules vendored
View File

@ -1,12 +0,0 @@
[submodule "libs/QSimpleUpdater"]
path = libs/QSimpleUpdater
url = https://github.com/alex-spataru/QSimpleUpdater
[submodule "libs/qtcsv"]
path = libs/qtcsv
url = https://github.com/iamantony/qtcsv
[submodule "doc/doxygen/doxygen-awesome-css"]
path = doc/doxygen/doxygen-awesome-css
url = https://github.com/jothepro/doxygen-awesome-css.git
[submodule "libs/qmqtt"]
path = libs/qmqtt
url = https://github.com/alex-spataru/qmqtt

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +0,0 @@
# The hacker's notebook (WIP)
Hi! If you are reading this, you are probably trying to understand how this software works internally. **Good for you!**
In general, I try to write [DOXYGEN](https://www.doxygen.nl/index.html) comments to explain what each function does. *However*, this does not help newcomers understand how a program's source code is organized (or how it *works*). For this reason, I will try to explain what each folder & class does in this document.
Think of this file as an *introductory document* for colaborators, hackers and geeks.
Of course, if this documentation is not clear enough (or you are a good writer), you are very welcome to fork this project, modify it & create a [pull request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests).
### But I'm not a coder!
Don't worry! You can help this project by reporting bugs, providing ideas, creating translations, writing about it in your blog/vlog, etc. You can also [donate](https://www.paypal.com/donate?hosted_button_id=XN68J47QJKYDE) through PayPal!
## Software architecture
Before explaining what each module does, please take a look at the "high-level" architecture of Serial Studio. If you have any doubts, feel free to use any of the support channels provided by the maintainers of the project.
![Architecture](architecture.svg)
### Source code organization
Since there are over 30 source files that compromise this project (not including any libraries or the [UI/QML](https://github.com/Serial-Studio/Serial-Studio/tree/master/assets/qml) part), it is important to keep the files organized in subdirectories corresponding to the their respective high-level functionalities.
Here is a breakdown of each subdirectory of the source code:
- The **CSV** directory contains the `CSV::Export` module and the `CSV::Player` functionality.
- The **IO** folder contains the `IO::Manager`, handlers for device data sources `IO::DataSources::Serial`, `IO::DataSources::Network` & `IO::Console` data handler.
- The I/O manager & console are implemented as [singleton classes](https://en.wikipedia.org/wiki/Singleton_pattern).
- The **JSON** folder contains the following classes:
- A `JSON::Frame` object represents the title, groups & datasets of a single frame. The frame object is generated by combining information received from the connected device and the JSON map file.
- The `JSON::Group` class represents a group object. Groups contain a title and an array of datasets.
- The `JSON::Dataset` class represents a dataset object. Datasets contain a title, a value and the units in which the dataset is measured (e.g. volts, meters, seconds, etc).
- The `JSON::Generator` class receives data from the `I/O Manager` and uses the JSON map file to generate a JSON document that is used to create a `Frame` object.
- `JSON::FrameInfo` (JFI) implements a structure that contains a JSON frame, the RX date/time and the frame ID number.
- The `JSON::Editor` provides the necessary logic to allow users to build JSON files directly from the user interface.
- The **Misc** directory contains several utility classes, such as the global `Misc::TimerEvents` class, which is used to update the UI elements at a given frequency. Or the `Misc::ModuleManager`, which is used to initialize the application.
- The `MQTT::Client` module contains a singleton class that allows Serial Studio to act as an [MQTT](https://en.wikipedia.org/wiki/MQTT) client.
- The **Plugins** folder contains a simple `Plugins::Server` that allows Serial Studio to interact with external applications/plugins.
- The **UI** folder contains the QML data provider, which provides QML-friendly functions so that the QML user interface can represent the lastest `JSON::Frame` object.
- The **Widgets** directory contains several `QWidget` based controls adapted to be used with a QML interface, most of the user interface elements that display data are implemented here. Most of the widgets are implemented with the help of [Qwt](https://qwt.sourceforge.io/).
**What about the user interface?**
The user interface is written in [QtQuick/QML](https://doc.qt.io/qt-5/qtquick-index.html). You can find the "source code" of the user interface in the `$PROJ_ROOT/assets/qml` folder.
## Coding styles
If possible, please follow the coding conventions defined in the `.clang-format` file. If you are as lazy as me, you can download and install [clang-format-all](https://github.com/eklitzke/clang-format-all) and run the following command before making a commit:
> `clang-format-all src`
This will apply the style defined in the `.clang-format` file recursively to all C++ headers and sources in the project. No need to worry about writing "ugly" code anymore!
**IMPORTANT! Please write explanatory comments to document your changes!**

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 47 KiB

@ -1 +0,0 @@
Subproject commit 9380569e8aea36374d848c8a43c6f9c6343d9bc0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

@ -1 +0,0 @@
Subproject commit 62e0ce7dde4c76e7533f2f0ee53f19ee1690c8dd

View File

@ -0,0 +1,38 @@
BasedOnStyle: LLVM
Standard: Cpp11
CommentPragmas: "^!|^:"
PointerBindsToType: false
SpaceAfterTemplateKeyword: false
BreakBeforeBinaryOperators: All
BreakBeforeBraces: Custom
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeComma
ConstructorInitializerIndentWidth: 2
NamespaceIndentation: None
AlignAfterOpenBracket: true
AlwaysBreakTemplateDeclarations: true
AllowShortFunctionsOnASingleLine: Inline
SortIncludes: false
IndentCaseLabels: true
IndentPPDirectives: AfterHash
AccessModifierOffset: -2
IndentWidth: 2
ColumnLimit: 80
BraceWrapping:
AfterClass: true
AfterControlStatement: true
AfterEnum: true
AfterFunction: true
AfterNamespace: true
AfterObjCDeclaration: true
AfterStruct: true
AfterUnion: true
BeforeCatch: true
BeforeElse: true
IndentBraces: false
ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE ]
#StatementMacros ['Q_OBJECT', 'Q_UNUSED']

View File

@ -0,0 +1,111 @@
#
# Copyright (c) 2024 Alex Spataru <https://github.com/alex-spataru>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#-------------------------------------------------------------------------------
# Project setup
#-------------------------------------------------------------------------------
cmake_minimum_required(VERSION 3.19)
project(QSimpleUpdater CXX)
#-------------------------------------------------------------------------------
# C++ options
#-------------------------------------------------------------------------------
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
#-------------------------------------------------------------------------------
# Add external dependencies
#-------------------------------------------------------------------------------
find_package(Qt6 COMPONENTS Core Gui Network Widgets REQUIRED)
#-------------------------------------------------------------------------------
# Fix linking errors
#-------------------------------------------------------------------------------
add_definitions(-DQSU_INCLUDE_MOC=1)
#-------------------------------------------------------------------------------
# Import source code & resources
#-------------------------------------------------------------------------------
include_directories(include)
set(SOURCES
src/Updater.cpp
src/Downloader.cpp
src/QSimpleUpdater.cpp
src/Downloader.ui
)
set(HEADERS
src/Updater.h
src/Downloader.h
include/QSimpleUpdater.h
)
#-------------------------------------------------------------------------------
# Compile & link the library
#-------------------------------------------------------------------------------
qt_add_resources(QSU_RCC ${CMAKE_CURRENT_SOURCE_DIR}/etc/resources/qsimpleupdater.qrc)
add_library(
QSimpleUpdater
STATIC
${SOURCES}
${HEADERS}
${QSU_RCC}
)
target_link_libraries(
QSimpleUpdater PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Network
Qt6::Widgets
)
target_include_directories(
QSimpleUpdater PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/include
)
#-------------------------------------------------------------------------------
# Configure unity build
#-------------------------------------------------------------------------------
set_target_properties(
QSimpleUpdater PROPERTIES
UNITY_BUILD ON
UNITY_BUILD_MODE BATCH
UNITY_BUILD_BATCH_SIZE 128
INTERPROCEDURAL_OPTIMIZATION TRUE
)

View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at alex_spataru@outlook.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -0,0 +1,21 @@
# License
Copyright (c) 2014-2021 [Alex Spataru](https://github.com/alex-spataru).
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,42 @@
#
# Copyright (c) 2014-2021 Alex Spataru <https://github.com/alex-spataru>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
QT += gui
QT += core
QT += network
QT += widgets
DEFINES += QSU_INCLUDE_MOC=1
INCLUDEPATH += $$PWD/include
SOURCES += \
$$PWD/src/Updater.cpp \
$$PWD/src/Downloader.cpp \
$$PWD/src/QSimpleUpdater.cpp
HEADERS += \
$$PWD/include/QSimpleUpdater.h \
$$PWD/src/Updater.h \
$$PWD/src/Downloader.h
FORMS += $$PWD/src/Downloader.ui
RESOURCES += $$PWD/etc/resources/qsimpleupdater.qrc

View File

@ -0,0 +1,25 @@
#
# Copyright (c) 2014-2021 Alex Spataru <https://github.com/alex-spataru>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
TEMPLATE = lib
DEFINES += QSU_SHARED
include ($$PWD/QSimpleUpdater.pri)

View File

@ -0,0 +1,79 @@
<a href="#">
<img width="112px" height="112px" src="etc/icon.svg" align="right" />
</a>
# QSimpleUpdater
[![Build Status](https://github.com/alex-spataru/QSimpleUpdater/workflows/Build/badge.svg)](#)
QSimpleUpdater is an implementation of an auto-updating system to be used with Qt projects. It allows you to easily check for updates, download them and install them. Additionally, the QSimpleUpdater allows you to check for updates for different "modules" of your application. Check the [FAQ](#faq) for more information.
Online documentation can be found [here](http://frc-utilities.github.io/documentation/qsimpleupdater/).
[![Downloading](etc/screenshots/downloading.png)](etc/screenshots/)
## Integrating QSimpleUpdater with your projects
1. Copy the QSimpleUpdater folder in your "3rd-party" folder.
2. Include the QSimpleUpdater project include (*pri*) file using the include() function.
3. That's all! Check the [tutorial project](/tutorial) as a reference for your project.
## FAQ
### 1. How does the QSimpleUpdater check for updates?
The QSimpleUpdater downloads an update definition file stored in JSON format. This file specifies the latest version, the download links and changelogs for each platform (you can also register your own platform easily if needed).
After downloading this file, the library analyzes the local version and the remote version. If the remote version is greater than the local version, then the library infers that there is an update available and notifies the user.
An example update definition file can be found [here](https://github.com/alex-spataru/QSimpleUpdater/blob/master/tutorial/definitions/updates.json).
### 2. Can I customize the update notifications shown to the user?
Yes! You can "toggle" which notifications to show using the library's functions or re-implement by yourself the notifications by "reacting" to the signals emitted by the QSimpleUpdater.
```c++
QString url = "https://MyBadassApplication.com/updates.json";
QSimpleUpdater::getInstance()->setNotifyOnUpdate (url, true);
QSimpleUpdater::getInstance()->setNotifyOnFinish (url, false);
QSimpleUpdater::getInstance()->checkForUpdates (url);
```
### 3. Is the application able to download the updates directly?
Yes. If there is an update available, the library will prompt the user if he/she wants to download the update. You can enable or disable the integrated downloader with the following code:
```c++
QString url = "https://MyBadassApplication.com/updates.json";
QSimpleUpdater::getInstance()->setDownloaderEnabled (url, true);
```
### 4. Why do I need to specify an URL for each function of the library?
The QSimpleUpdater allows you to use different updater instances, which can be accessed with the URL of the update definitions.
While it is not obligatory to use multiple updater instances, this can be useful for applications that make use of plugins or different modules.
Say that you are developing a game, in this case, you could use the following code:
```c++
// Update the game textures
QString textures_url = "https://MyBadassGame.com/textures.json"
QSimpleUpdater::getInstance()->setModuleName (textures_url, "textures");
QSimpleUpdater::getInstance()->setModuleVersion (textures_url, "0.4");
QSimpleUpdater::getInstance()->checkForUpdates (textures_url);
// Update the game sounds
QString sounds_url = "https://MyBadassGame.com/sounds.json"
QSimpleUpdater::getInstance()->setModuleName (sounds_url, "sounds");
QSimpleUpdater::getInstance()->setModuleVersion (sounds_url, "0.6");
QSimpleUpdater::getInstance()->checkForUpdates (sounds_url);
// Update the client (name & versions are already stored in qApp)
QString client_url = "https://MyBadassGame.com/client.json"
QSimpleUpdater::getInstance()->checkForUpdates (client_url);
```
## License
QSimpleUpdater is free and open-source software, it is released under the [MIT](LICENSE.md) license.

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="72pt" height="72pt" viewBox="0 0 72 72" version="1.1">
<g id="surface1993">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(36.862746%,61.176473%,46.27451%);fill-opacity:1;" d="M 70.199219 70.199219 L 1.800781 70.199219 L 1.800781 53.101562 L 3.601562 53.101562 L 3.601562 68.398438 L 68.398438 68.398438 L 68.398438 53.101562 L 70.199219 53.101562 Z M 70.199219 70.199219 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(72.941178%,87.843138%,74.117649%);fill-opacity:1;" d="M 31.859375 58.949219 L 29.429688 50.671875 L 29.25 50.578125 C 26.550781 49.679688 24.121094 48.238281 21.960938 46.351562 L 21.78125 46.171875 L 13.769531 47.96875 L 9.628906 40.5 L 15.390625 34.738281 L 15.390625 34.46875 C 15.121094 33.121094 15.03125 31.769531 15.03125 30.421875 C 15.03125 29.070312 15.121094 27.71875 15.390625 26.371094 L 15.390625 26.101562 L 9.628906 20.339844 L 13.769531 12.871094 L 21.78125 14.671875 L 21.960938 14.488281 C 24.121094 12.601562 26.550781 11.160156 29.25 10.261719 L 29.429688 10.171875 L 31.859375 1.890625 L 40.140625 1.890625 L 42.570312 10.171875 L 42.75 10.261719 C 45.449219 11.160156 47.878906 12.601562 50.039062 14.488281 L 50.21875 14.671875 L 58.230469 12.871094 L 62.371094 20.339844 L 56.699219 26.371094 L 56.699219 26.640625 C 56.96875 27.988281 57.058594 29.339844 57.058594 30.691406 C 57.058594 32.039062 56.96875 33.390625 56.699219 34.738281 L 56.699219 35.011719 L 62.460938 40.769531 L 58.320312 48.238281 L 50.308594 46.441406 L 50.128906 46.621094 C 47.96875 48.511719 45.539062 49.949219 42.839844 50.851562 L 42.660156 50.941406 L 40.230469 59.21875 L 31.859375 59.21875 Z M 36 21.148438 C 30.78125 21.148438 26.550781 25.378906 26.550781 30.601562 C 26.550781 35.820312 30.78125 40.050781 36 40.050781 C 41.21875 40.050781 45.449219 35.820312 45.449219 30.601562 C 45.449219 25.378906 41.21875 21.148438 36 21.148438 Z M 36 21.148438 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(36.862746%,61.176473%,46.27451%);fill-opacity:1;" d="M 39.78125 2.699219 L 42.03125 10.351562 L 42.121094 10.800781 L 42.570312 10.980469 C 45.179688 11.878906 47.609375 13.230469 49.679688 15.121094 L 50.039062 15.390625 L 50.488281 15.300781 L 57.960938 13.589844 L 61.828125 20.519531 L 56.519531 25.828125 L 56.160156 26.191406 L 56.25 26.640625 C 56.519531 27.898438 56.609375 29.25 56.609375 30.601562 C 56.609375 31.949219 56.519531 33.210938 56.25 34.558594 L 56.160156 35.011719 L 61.828125 40.679688 L 57.960938 47.609375 L 50.578125 45.898438 L 50.128906 45.808594 L 49.769531 46.078125 C 47.699219 47.96875 45.269531 49.320312 42.660156 50.21875 L 42.210938 50.398438 L 42.121094 50.851562 L 39.78125 58.5 L 32.128906 58.5 L 29.878906 50.851562 L 29.789062 50.398438 L 29.339844 50.21875 C 26.730469 49.320312 24.300781 47.96875 22.230469 46.078125 L 21.871094 45.808594 L 21.421875 45.898438 L 13.949219 47.609375 L 10.078125 40.679688 L 15.390625 35.371094 L 15.75 35.011719 L 15.660156 34.558594 C 15.390625 33.210938 15.300781 31.859375 15.300781 30.601562 C 15.300781 29.339844 15.390625 27.988281 15.660156 26.640625 L 15.75 26.191406 L 15.390625 25.828125 L 10.078125 20.519531 L 13.949219 13.589844 L 21.421875 15.300781 L 21.871094 15.390625 L 22.230469 15.121094 C 24.300781 13.230469 26.730469 11.878906 29.339844 10.980469 L 29.789062 10.800781 L 29.878906 10.351562 L 32.21875 2.699219 L 39.78125 2.699219 M 36 40.5 C 41.488281 40.5 45.898438 36.089844 45.898438 30.601562 C 45.898438 25.109375 41.488281 20.699219 36 20.699219 C 30.511719 20.699219 26.101562 25.109375 26.101562 30.601562 C 26.101562 36.089844 30.511719 40.5 36 40.5 M 40.5 1.800781 L 31.5 1.800781 L 29.070312 10.171875 C 26.28125 11.070312 23.761719 12.601562 21.601562 14.488281 L 13.5 12.601562 L 9 20.699219 L 14.761719 26.460938 C 14.488281 27.808594 14.398438 29.160156 14.398438 30.601562 C 14.398438 32.039062 14.578125 33.390625 14.761719 34.738281 L 9 40.5 L 13.5 48.601562 L 21.691406 46.710938 C 23.851562 48.601562 26.371094 50.128906 29.160156 51.03125 L 31.5 59.398438 L 40.5 59.398438 L 42.929688 51.03125 C 45.71875 50.128906 48.238281 48.601562 50.398438 46.710938 L 58.5 48.601562 L 63 40.5 L 57.238281 34.738281 C 57.511719 33.390625 57.601562 32.039062 57.601562 30.601562 C 57.601562 29.160156 57.421875 27.808594 57.238281 26.460938 L 63 20.699219 L 58.5 12.601562 L 50.308594 14.488281 C 48.148438 12.601562 45.628906 11.070312 42.839844 10.171875 Z M 36 39.601562 C 31.050781 39.601562 27 35.550781 27 30.601562 C 27 25.648438 31.050781 21.601562 36 21.601562 C 40.949219 21.601562 45 25.648438 45 30.601562 C 45 35.550781 40.949219 39.601562 36 39.601562 Z M 36 39.601562 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/icons">
<file alias="update.png">update.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -0,0 +1,108 @@
/*
* Copyright (c) 2014-2021 Alex Spataru <https://github.com/alex-spataru>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef _QSIMPLEUPDATER_MAIN_H
#define _QSIMPLEUPDATER_MAIN_H
#include <QUrl>
#include <QList>
#include <QObject>
#if defined(QSIMPLEUPDATER_BUILD_DLIB)
# define QSU_SHARED_EXPORT Q_DECL_EXPORT
#elif defined(QSIMPLEUPDATER_USE_DLIB)
# define QSU_SHARED_EXPORT Q_DECL_IMPORT
#else
# define QSU_SHARED_EXPORT
#endif
class Updater;
/**
* \brief Manages the updater instances
*
* The \c QSimpleUpdater class manages the updater system and allows for
* parallel application modules to check for updates and download them.
*
* The behavior of each updater can be regulated by specifying the update
* definitions URL (from where we download the individual update definitions)
* and defining the desired options by calling the individual "setter"
* functions (e.g. \c setNotifyOnUpdate()).
*
* The \c QSimpleUpdater also implements an integrated downloader.
* If you need to use a custom install procedure/code, just create a function
* that is called when the \c downloadFinished() signal is emitted to
* implement your own install procedures.
*
* By default, the downloader will try to open the file as if you opened it
* from a file manager or a web browser (with the "file:*" url).
*/
class QSU_SHARED_EXPORT QSimpleUpdater : public QObject
{
Q_OBJECT
signals:
void checkingFinished(const QString &url);
void appcastDownloaded(const QString &url, const QByteArray &data);
void downloadFinished(const QString &url, const QString &filepath);
public:
static QSimpleUpdater *getInstance();
bool usesCustomAppcast(const QString &url) const;
bool getNotifyOnUpdate(const QString &url) const;
bool getNotifyOnFinish(const QString &url) const;
bool getUpdateAvailable(const QString &url) const;
bool getDownloaderEnabled(const QString &url) const;
bool usesCustomInstallProcedures(const QString &url) const;
QString getOpenUrl(const QString &url) const;
QString getChangelog(const QString &url) const;
QString getModuleName(const QString &url) const;
QString getDownloadUrl(const QString &url) const;
QString getPlatformKey(const QString &url) const;
QString getLatestVersion(const QString &url) const;
QString getModuleVersion(const QString &url) const;
QString getUserAgentString(const QString &url) const;
public slots:
void checkForUpdates(const QString &url);
void setModuleName(const QString &url, const QString &name);
void setNotifyOnUpdate(const QString &url, const bool notify);
void setNotifyOnFinish(const QString &url, const bool notify);
void setPlatformKey(const QString &url, const QString &platform);
void setModuleVersion(const QString &url, const QString &version);
void setDownloaderEnabled(const QString &url, const bool enabled);
void setUserAgentString(const QString &url, const QString &agent);
void setUseCustomAppcast(const QString &url, const bool customAppcast);
void setUseCustomInstallProcedures(const QString &url, const bool custom);
void setMandatoryUpdate(const QString &url, const bool mandatory_update);
protected:
QSimpleUpdater();
~QSimpleUpdater();
private:
Updater *getUpdater(const QString &url) const;
};
#endif

View File

@ -0,0 +1,469 @@
/*
* Copyright (c) 2014-2021 Alex Spataru <https://github.com/alex-spataru>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QDateTime>
#include <QMessageBox>
#include <QNetworkReply>
#include <QDesktopServices>
#include <QNetworkAccessManager>
#include <math.h>
#include "Downloader.h"
static const QString PARTIAL_DOWN(".part");
Downloader::Downloader(QWidget *parent)
: QWidget(parent)
{
m_ui = new Ui::Downloader;
m_ui->setupUi(this);
/* Initialize private members */
m_manager = new QNetworkAccessManager();
/* Initialize internal values */
m_url = "";
m_fileName = "";
m_startTime = 0;
m_useCustomProcedures = false;
m_mandatoryUpdate = false;
/* Set download directory */
m_downloadDir.setPath(QDir::homePath() + "/Downloads/");
/* Make the window look like a modal dialog */
setWindowIcon(QIcon());
setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
/* Configure the appearance and behavior of the buttons */
m_ui->openButton->setEnabled(false);
m_ui->openButton->setVisible(false);
connect(m_ui->stopButton, SIGNAL(clicked()), this, SLOT(cancelDownload()));
connect(m_ui->openButton, SIGNAL(clicked()), this, SLOT(installUpdate()));
/* Resize to fit */
setFixedSize(minimumSizeHint());
}
Downloader::~Downloader()
{
delete m_ui;
delete m_reply;
delete m_manager;
}
/**
* Returns \c true if the updater shall not intervene when the download has
* finished (you can use the \c QSimpleUpdater signals to know when the
* download is completed).
*/
bool Downloader::useCustomInstallProcedures() const
{
return m_useCustomProcedures;
}
/**
* Changes the URL, which is used to indentify the downloader dialog
* with an \c Updater instance
*
* \note the \a url parameter is not the download URL, it is the URL of
* the AppCast file
*/
void Downloader::setUrlId(const QString &url)
{
m_url = url;
}
/**
* Begins downloading the file at the given \a url
*/
void Downloader::startDownload(const QUrl &url)
{
/* Reset UI */
m_ui->progressBar->setValue(0);
m_ui->stopButton->setText(tr("Stop"));
m_ui->downloadLabel->setText(tr("Downloading updates"));
m_ui->timeLabel->setText(tr("Time remaining") + ": " + tr("unknown"));
/* Configure the network request */
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
if (!m_userAgentString.isEmpty())
request.setRawHeader("User-Agent", m_userAgentString.toUtf8());
/* Start download */
m_reply = m_manager->get(request);
m_startTime = QDateTime::currentDateTime().toSecsSinceEpoch();
/* Ensure that downloads directory exists */
if (!m_downloadDir.exists())
m_downloadDir.mkpath(".");
/* Remove old downloads */
QFile::remove(m_downloadDir.filePath(m_fileName));
QFile::remove(m_downloadDir.filePath(m_fileName + PARTIAL_DOWN));
/* Update UI when download progress changes or download finishes */
connect(m_reply, SIGNAL(downloadProgress(qint64, qint64)), this,
SLOT(updateProgress(qint64, qint64)));
connect(m_reply, SIGNAL(finished()), this, SLOT(finished()));
showNormal();
}
/**
* Changes the name of the downloaded file
*/
void Downloader::setFileName(const QString &file)
{
m_fileName = file;
if (m_fileName.isEmpty())
m_fileName = "QSU_Update.bin";
}
/**
* Changes the user-agent string used to communicate with the remote HTTP server
*/
void Downloader::setUserAgentString(const QString &agent)
{
m_userAgentString = agent;
}
void Downloader::finished()
{
/* Rename file */
QFile::rename(m_downloadDir.filePath(m_fileName + PARTIAL_DOWN),
m_downloadDir.filePath(m_fileName));
/* Notify application */
emit downloadFinished(m_url, m_downloadDir.filePath(m_fileName));
/* Install the update */
m_reply->close();
installUpdate();
setVisible(false);
}
/**
* Opens the downloaded file.
* \note If the downloaded file is not found, then the function will alert the
* user about the error.
*/
void Downloader::openDownload()
{
if (!m_fileName.isEmpty())
QDesktopServices::openUrl(
QUrl::fromLocalFile(m_downloadDir.filePath(m_fileName)));
else
{
QMessageBox::critical(this, tr("Error"),
tr("Cannot find downloaded update!"),
QMessageBox::Close);
}
}
/**
* Instructs the OS to open the downloaded file.
*
* \note If \c useCustomInstallProcedures() returns \c true, the function will
* not instruct the OS to open the downloaded file. You can use the
* signals fired by the \c QSimpleUpdater to install the update with your
* own implementations/code.
*/
void Downloader::installUpdate()
{
if (useCustomInstallProcedures())
return;
/* Update labels */
m_ui->stopButton->setText(tr("Close"));
m_ui->downloadLabel->setText(tr("Download complete!"));
m_ui->timeLabel->setText(tr("The installer will open separately") + "...");
/* Ask the user to install the download */
QMessageBox box;
box.setIcon(QMessageBox::Question);
box.setDefaultButton(QMessageBox::Ok);
box.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
box.setInformativeText(tr("Click \"OK\" to begin installing the update"));
QString text = tr("In order to install the update, you may need to "
"quit the application.");
if (m_mandatoryUpdate)
text = tr("In order to install the update, you may need to "
"quit the application. This is a mandatory update, exiting now "
"will close the application");
box.setText("<h3>" + text + "</h3>");
/* User wants to install the download */
if (box.exec() == QMessageBox::Ok)
{
if (!useCustomInstallProcedures())
openDownload();
}
/* Wait */
else
{
if (m_mandatoryUpdate)
QApplication::quit();
m_ui->openButton->setEnabled(true);
m_ui->openButton->setVisible(true);
m_ui->timeLabel->setText(tr("Click the \"Open\" button to "
"apply the update"));
}
}
/**
* Prompts the user if he/she wants to cancel the download and cancels the
* download if the user agrees to do that.
*/
void Downloader::cancelDownload()
{
if (!m_reply->isFinished())
{
QMessageBox box;
box.setWindowTitle(tr("Updater"));
box.setIcon(QMessageBox::Question);
box.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
QString text = tr("Are you sure you want to cancel the download?");
if (m_mandatoryUpdate)
{
text = tr("Are you sure you want to cancel the download? This is a "
"mandatory update, exiting now will close "
"the application");
}
box.setText(text);
if (box.exec() == QMessageBox::Yes)
{
hide();
m_reply->abort();
if (m_mandatoryUpdate)
QApplication::quit();
}
}
else
{
if (m_mandatoryUpdate)
QApplication::quit();
hide();
}
}
/**
* Writes the downloaded data to the disk
*/
void Downloader::saveFile(qint64 received, qint64 total)
{
Q_UNUSED(received);
Q_UNUSED(total);
/* Check if we need to redirect */
QUrl url
= m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (!url.isEmpty())
{
startDownload(url);
return;
}
/* Save downloaded data to disk */
QFile file(m_downloadDir.filePath(m_fileName + PARTIAL_DOWN));
if (file.open(QIODevice::WriteOnly | QIODevice::Append))
{
file.write(m_reply->readAll());
file.close();
}
}
/**
* Calculates the appropiate size units (bytes, KB or MB) for the received
* data and the total download size. Then, this function proceeds to update the
* dialog controls/UI.
*/
void Downloader::calculateSizes(qint64 received, qint64 total)
{
QString totalSize;
QString receivedSize;
if (total < 1024)
totalSize = tr("%1 bytes").arg(total);
else if (total < 1048576)
totalSize = tr("%1 KB").arg(round(total / 1024));
else
totalSize = tr("%1 MB").arg(round(total / 1048576));
if (received < 1024)
receivedSize = tr("%1 bytes").arg(received);
else if (received < 1048576)
receivedSize = tr("%1 KB").arg(received / 1024);
else
receivedSize = tr("%1 MB").arg(received / 1048576);
m_ui->downloadLabel->setText(tr("Downloading updates") + " (" + receivedSize
+ " " + tr("of") + " " + totalSize + ")");
}
/**
* Uses the \a received and \a total parameters to get the download progress
* and update the progressbar value on the dialog.
*/
void Downloader::updateProgress(qint64 received, qint64 total)
{
if (total > 0)
{
m_ui->progressBar->setMinimum(0);
m_ui->progressBar->setMaximum(100);
m_ui->progressBar->setValue((received * 100) / total);
calculateSizes(received, total);
calculateTimeRemaining(received, total);
saveFile(received, total);
}
else
{
m_ui->progressBar->setMinimum(0);
m_ui->progressBar->setMaximum(0);
m_ui->progressBar->setValue(-1);
m_ui->downloadLabel->setText(tr("Downloading Updates") + "...");
m_ui->timeLabel->setText(
QString("%1: %2").arg(tr("Time Remaining")).arg(tr("Unknown")));
}
}
/**
* Uses two time samples (from the current time and a previous sample) to
* calculate how many bytes have been downloaded.
*
* Then, this function proceeds to calculate the appropiate units of time
* (hours, minutes or seconds) and constructs a user-friendly string, which
* is displayed in the dialog.
*/
void Downloader::calculateTimeRemaining(qint64 received, qint64 total)
{
uint difference
= QDateTime::currentDateTime().toSecsSinceEpoch() - m_startTime;
if (difference > 0)
{
QString timeString;
qreal timeRemaining = (total - received) / (received / difference);
if (timeRemaining > 7200)
{
timeRemaining /= 3600;
int hours = int(timeRemaining + 0.5);
if (hours > 1)
timeString = tr("about %1 hours").arg(hours);
else
timeString = tr("about one hour");
}
else if (timeRemaining > 60)
{
timeRemaining /= 60;
int minutes = int(timeRemaining + 0.5);
if (minutes > 1)
timeString = tr("%1 minutes").arg(minutes);
else
timeString = tr("1 minute");
}
else if (timeRemaining <= 60)
{
int seconds = int(timeRemaining + 0.5);
if (seconds > 1)
timeString = tr("%1 seconds").arg(seconds);
else
timeString = tr("1 second");
}
m_ui->timeLabel->setText(tr("Time remaining") + ": " + timeString);
}
}
/**
* Rounds the given \a input to two decimal places
*/
qreal Downloader::round(const qreal &input)
{
return static_cast<qreal>(roundf(static_cast<float>(input) * 100) / 100);
}
QString Downloader::downloadDir() const
{
return m_downloadDir.absolutePath();
}
void Downloader::setDownloadDir(const QString &downloadDir)
{
if (m_downloadDir.absolutePath() != downloadDir)
m_downloadDir.setPath(downloadDir);
}
/**
* If the \a mandatory_update is set to \c true, the \c Downloader has to
* download and install the update. If the user cancels or exits, the
* application will close
*/
void Downloader::setMandatoryUpdate(const bool mandatory_update)
{
m_mandatoryUpdate = mandatory_update;
}
/**
* If the \a custom parameter is set to \c true, then the \c Downloader will not
* attempt to open the downloaded file.
*
* Use the signals fired by the \c QSimpleUpdater to implement your own install
* procedures.
*/
void Downloader::setUseCustomInstallProcedures(const bool custom)
{
m_useCustomProcedures = custom;
}
#if QSU_INCLUDE_MOC
# include "moc_Downloader.cpp"
#endif

View File

@ -0,0 +1,93 @@
/*
* Copyright (c) 2014-2021 Alex Spataru <https://github.com/alex-spataru>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef DOWNLOAD_DIALOG_H
#define DOWNLOAD_DIALOG_H
#include <QDir>
#include <QDialog>
#include <ui_Downloader.h>
namespace Ui
{
class Downloader;
}
class QNetworkReply;
class QNetworkAccessManager;
/**
* \brief Implements an integrated file downloader with a nice UI
*/
class Downloader : public QWidget
{
Q_OBJECT
signals:
void downloadFinished(const QString &url, const QString &filepath);
public:
explicit Downloader(QWidget *parent = 0);
~Downloader();
bool useCustomInstallProcedures() const;
QString downloadDir() const;
void setDownloadDir(const QString &downloadDir);
public slots:
void setUrlId(const QString &url);
void startDownload(const QUrl &url);
void setFileName(const QString &file);
void setUserAgentString(const QString &agent);
void setUseCustomInstallProcedures(const bool custom);
void setMandatoryUpdate(const bool mandatory_update);
private slots:
void finished();
void openDownload();
void installUpdate();
void cancelDownload();
void saveFile(qint64 received, qint64 total);
void calculateSizes(qint64 received, qint64 total);
void updateProgress(qint64 received, qint64 total);
void calculateTimeRemaining(qint64 received, qint64 total);
private:
qreal round(const qreal &input);
private:
QString m_url;
uint m_startTime;
QDir m_downloadDir;
QString m_fileName;
Ui::Downloader *m_ui;
QNetworkReply *m_reply;
QString m_userAgentString;
bool m_useCustomProcedures;
bool m_mandatoryUpdate;
QNetworkAccessManager *m_manager;
};
#endif

View File

@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Downloader</class>
<widget class="QWidget" name="Downloader">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>420</width>
<height>136</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Updater</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>8</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>8</number>
</property>
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>8</number>
</property>
<property name="topMargin">
<number>8</number>
</property>
<property name="rightMargin">
<number>8</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="updater_icon">
<property name="minimumSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../etc/resources/qsimpleupdater.qrc">:/icons/update.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="progressFrame" native="true">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="downloadLabel">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Downloading updates</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="minimumSize">
<size>
<width>320</width>
<height>0</height>
</size>
</property>
<property name="value">
<number>0</number>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="timeLabel">
<property name="text">
<string>Time remaining: 0 minutes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="buttonFrame" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>8</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>8</number>
</property>
<property name="bottomMargin">
<number>8</number>
</property>
<item>
<spacer name="buttonSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="openButton">
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopButton">
<property name="text">
<string>Stop</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../etc/resources/qsimpleupdater.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -0,0 +1,420 @@
/*
* Copyright (c) 2014-2021 Alex Spataru <https://github.com/alex-spataru>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "Updater.h"
#include "QSimpleUpdater.h"
static QList<QString> URLS;
static QList<Updater *> UPDATERS;
QSimpleUpdater::QSimpleUpdater()
{
Q_INIT_RESOURCE(qsimpleupdater);
}
QSimpleUpdater::~QSimpleUpdater()
{
URLS.clear();
foreach (Updater *updater, UPDATERS)
updater->deleteLater();
UPDATERS.clear();
}
/**
* Returns the only instance of the class
*/
QSimpleUpdater *QSimpleUpdater::getInstance()
{
static QSimpleUpdater updater;
return &updater;
}
/**
* Returns \c true if the \c Updater instance registered with the given \a url
* uses a custom appcast format and/or allows the application to read and
* interpret the downloaded appcast file
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
bool QSimpleUpdater::usesCustomAppcast(const QString &url) const
{
return getUpdater(url)->customAppcast();
}
/**
* Returns \c true if the \c Updater instance registered with the given \a url
* shall notify the user when an update is available.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
bool QSimpleUpdater::getNotifyOnUpdate(const QString &url) const
{
return getUpdater(url)->notifyOnUpdate();
}
/**
* Returns \c true if the \c Updater instance registered with the given \a url
* shall notify the user when it finishes checking for updates.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
bool QSimpleUpdater::getNotifyOnFinish(const QString &url) const
{
return getUpdater(url)->notifyOnFinish();
}
/**
* Returns \c true if the \c Updater instance registered with the given \a url
* has an update available.
*
* \warning You should call \c checkForUpdates() before using this function
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
bool QSimpleUpdater::getUpdateAvailable(const QString &url) const
{
return getUpdater(url)->updateAvailable();
}
/**
* Returns \c true if the \c Updater instance registered with the given \a url
* has the integrated downloader enabled.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
bool QSimpleUpdater::getDownloaderEnabled(const QString &url) const
{
return getUpdater(url)->downloaderEnabled();
}
/**
* Returns \c true if the \c Updater instance registered with the given \a url
* shall try to open the downloaded file.
*
* If you want to implement your own way to handle the downloaded file, just
* bind to the \c downloadFinished() signal and disable the integrated
* downloader with the \c setUseCustomInstallProcedures() function.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
bool QSimpleUpdater::usesCustomInstallProcedures(const QString &url) const
{
return getUpdater(url)->useCustomInstallProcedures();
}
/**
* Returns the URL to open in a web browser of the \c Updater instance
* registered with the given \a url.
*
* \note If the module name is empty, then the \c Updater will use the
* application name as its module name.
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
QString QSimpleUpdater::getOpenUrl(const QString &url) const
{
return getUpdater(url)->openUrl();
}
/**
* Returns the changelog of the \c Updater instance registered with the given
* \a url.
*
* \warning You should call \c checkForUpdates() before using this function
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
QString QSimpleUpdater::getChangelog(const QString &url) const
{
return getUpdater(url)->changelog();
}
/**
* Returns the module name of the \c Updater instance registered with the given
* \a url.
*
* \note If the module name is empty, then the \c Updater will use the
* application name as its module name.
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
QString QSimpleUpdater::getModuleName(const QString &url) const
{
return getUpdater(url)->moduleName();
}
/**
* Returns the download URL of the \c Updater instance registered with the given
* \a url.
*
* \warning You should call \c checkForUpdates() before using this function
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
QString QSimpleUpdater::getDownloadUrl(const QString &url) const
{
return getUpdater(url)->downloadUrl();
}
/**
* Returns the platform key of the \c Updater registered with the given \a url.
* If you do not define a platform key, the system will assign the following
* platform key:
* - On iOS: \c ios
* - On Mac OSX: \c osx
* - On Android: \c android
* - On GNU/Linux: \c linux
* - On Microsoft Windows: \c windows
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
QString QSimpleUpdater::getPlatformKey(const QString &url) const
{
return getUpdater(url)->platformKey();
}
/**
* Returns the remote module version of the \c Updater instance registered with
* the given \a url.
*
* \warning You should call \c checkForUpdates() before using this function
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
QString QSimpleUpdater::getLatestVersion(const QString &url) const
{
return getUpdater(url)->latestVersion();
}
/**
* Returns the module version of the \c Updater instance registered with the
* given \a url.
*
* \note If the module version is empty, then the \c Updater will use the
* application version as its module version.
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
QString QSimpleUpdater::getModuleVersion(const QString &url) const
{
return getUpdater(url)->moduleVersion();
}
/**
* Returns the user-agent string used by the updater to communicate with
* the remote HTTP(S) server.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
QString QSimpleUpdater::getUserAgentString(const QString &url) const
{
return getUpdater(url)->userAgentString();
}
/**
* Instructs the \c Updater instance with the registered \c url to download and
* interpret the update definitions file.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
void QSimpleUpdater::checkForUpdates(const QString &url)
{
getUpdater(url)->checkForUpdates();
}
/**
* Changes the module \a name of the \c Updater instance registered at the
* given \a url.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
* \note The module name is used on the user prompts. If the module name is
* empty, then the prompts will show the name of the application.
*/
void QSimpleUpdater::setModuleName(const QString &url, const QString &name)
{
getUpdater(url)->setModuleName(name);
}
/**
* If \a notify is set to \c true, then the \c Updater instance registered with
* the given \a url will notify the user when an update is available.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
void QSimpleUpdater::setNotifyOnUpdate(const QString &url, const bool notify)
{
getUpdater(url)->setNotifyOnUpdate(notify);
}
/**
* If \a notify is set to \c true, then the \c Updater instance registered with
* the given \a url will notify the user when it has finished interpreting the
* update definitions file.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
void QSimpleUpdater::setNotifyOnFinish(const QString &url, const bool notify)
{
getUpdater(url)->setNotifyOnFinish(notify);
}
/**
* Changes the platform key of the \c Updater isntance registered at the given
* \a url.
*
* If the platform key is empty, then the system will use the following keys:
* - On iOS: \c ios
* - On Mac OSX: \c osx
* - On Android: \c android
* - On GNU/Linux: \c linux
* - On Microsoft Windows: \c windows
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
void QSimpleUpdater::setPlatformKey(const QString &url, const QString &platform)
{
getUpdater(url)->setPlatformKey(platform);
}
/**
* Changes the module \version of the \c Updater instance registered at the
* given \a url.
*
* \note The module version is used to compare it with the remove version.
* If the module name is empty, then the \c Updater instance will use the
* application version.
*/
void QSimpleUpdater::setModuleVersion(const QString &url,
const QString &version)
{
getUpdater(url)->setModuleVersion(version);
}
/**
* If the \a enabled parameter is set to \c true, the \c Updater instance
* registered with the given \a url will open the integrated downloader
* if the user agrees to install the update (if any).
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
void QSimpleUpdater::setDownloaderEnabled(const QString &url,
const bool enabled)
{
getUpdater(url)->setDownloaderEnabled(enabled);
}
/**
* Changes the user-agent string used by the updater to communicate
* with the remote server
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
void QSimpleUpdater::setUserAgentString(const QString &url,
const QString &agent)
{
getUpdater(url)->setUserAgentString(agent);
}
/**
* If the \a customAppcast parameter is set to \c true, then the \c Updater
* will not try to read the network reply from the server, instead, it will
* emit the \c appcastDownloaded() signal, which allows the application to
* read and interpret the appcast file by itself.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
void QSimpleUpdater::setUseCustomAppcast(const QString &url,
const bool customAppcast)
{
getUpdater(url)->setUseCustomAppcast(customAppcast);
}
/**
* If the \a custom parameter is set to \c true, the \c Updater instance
* registered with the given \a url will not try to open the downloaded file.
*
* If you want to implement your own way to handle the downloaded file, just
* bind to the \c downloadFinished() signal and disable the integrated
* downloader with the \c setUseCustomInstallProcedures() function.
*
* \note If an \c Updater instance registered with the given \a url is not
* found, that \c Updater instance will be initialized automatically
*/
void QSimpleUpdater::setUseCustomInstallProcedures(const QString &url,
const bool custom)
{
getUpdater(url)->setUseCustomInstallProcedures(custom);
}
void QSimpleUpdater::setMandatoryUpdate(const QString &url,
const bool mandatory_update)
{
getUpdater(url)->setMandatoryUpdate(mandatory_update);
}
/**
* Returns the \c Updater instance registered with the given \a url.
*
* If an \c Updater instance registered with teh given \a url does not exist,
* this function will create it and configure it automatically.
*/
Updater *QSimpleUpdater::getUpdater(const QString &url) const
{
if (!URLS.contains(url))
{
Updater *updater = new Updater;
updater->setUrl(url);
URLS.append(url);
UPDATERS.append(updater);
connect(updater, SIGNAL(checkingFinished(QString)), this,
SIGNAL(checkingFinished(QString)));
connect(updater, SIGNAL(downloadFinished(QString, QString)), this,
SIGNAL(downloadFinished(QString, QString)));
connect(updater, SIGNAL(appcastDownloaded(QString, QByteArray)), this,
SIGNAL(appcastDownloaded(QString, QByteArray)));
}
return UPDATERS.at(URLS.indexOf(url));
}
#if QSU_INCLUDE_MOC
# include "moc_QSimpleUpdater.cpp"
#endif

View File

@ -0,0 +1,525 @@
/*
* Copyright (c) 2014-2021 Alex Spataru <https://github.com/alex-spataru>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <QJsonValue>
#include <QJsonObject>
#include <QMessageBox>
#include <QApplication>
#include <QJsonDocument>
#include <QDesktopServices>
#include "Updater.h"
#include "Downloader.h"
Updater::Updater()
{
m_url = "";
m_openUrl = "";
m_changelog = "";
m_downloadUrl = "";
m_latestVersion = "";
m_customAppcast = false;
m_notifyOnUpdate = true;
m_notifyOnFinish = false;
m_updateAvailable = false;
m_downloaderEnabled = true;
m_moduleName = qApp->applicationName();
m_moduleVersion = qApp->applicationVersion();
m_mandatoryUpdate = false;
m_downloader = new Downloader();
m_manager = new QNetworkAccessManager();
#if defined Q_OS_WIN
m_platform = "windows";
#elif defined Q_OS_MAC
m_platform = "osx";
#elif defined Q_OS_LINUX
m_platform = "linux";
#elif defined Q_OS_ANDROID
m_platform = "android";
#elif defined Q_OS_IOS
m_platform = "ios";
#endif
setUserAgentString(
QString("%1/%2 (Qt; QSimpleUpdater)")
.arg(qApp->applicationName(), qApp->applicationVersion()));
connect(m_downloader, SIGNAL(downloadFinished(QString, QString)), this,
SIGNAL(downloadFinished(QString, QString)));
connect(m_manager, SIGNAL(finished(QNetworkReply *)), this,
SLOT(onReply(QNetworkReply *)));
}
Updater::~Updater()
{
delete m_downloader;
}
/**
* Returns the URL of the update definitions file
*/
QString Updater::url() const
{
return m_url;
}
/**
* Returns the URL that the update definitions file wants us to open in
* a web browser.
*
* \warning You should call \c checkForUpdates() before using this functio
*/
QString Updater::openUrl() const
{
return m_openUrl;
}
/**
* Returns the changelog defined by the update definitions file.
* \warning You should call \c checkForUpdates() before using this function
*/
QString Updater::changelog() const
{
return m_changelog;
}
/**
* Returns the name of the module (if defined)
*/
QString Updater::moduleName() const
{
return m_moduleName;
}
/**
* Returns the platform key (be it system-set or user-set).
* If you do not define a platform key, the system will assign the following
* platform key:
* - On iOS: \c ios
* - On Mac OSX: \c osx
* - On Android: \c android
* - On GNU/Linux: \c linux
* - On Microsoft Windows: \c windows
*/
QString Updater::platformKey() const
{
return m_platform;
}
/**
* Returns the download URL defined by the update definitions file.
* \warning You should call \c checkForUpdates() before using this function
*/
QString Updater::downloadUrl() const
{
return m_downloadUrl;
}
/**
* Returns the latest version defined by the update definitions file.
* \warning You should call \c checkForUpdates() before using this function
*/
QString Updater::latestVersion() const
{
return m_latestVersion;
}
/**
* Returns the user-agent header used by the client when communicating
* with the server through HTTP
*/
QString Updater::userAgentString() const
{
return m_userAgentString;
}
/**
* Returns the "local" version of the installed module
*/
QString Updater::moduleVersion() const
{
return m_moduleVersion;
}
/**
* Returns \c true if the updater should NOT interpret the downloaded appcast.
* This is useful if you need to store more variables (or information) in the
* JSON file or use another appcast format (e.g. XML)
*/
bool Updater::customAppcast() const
{
return m_customAppcast;
}
/**
* Returns \c true if the updater should notify the user when an update is
* available.
*/
bool Updater::notifyOnUpdate() const
{
return m_notifyOnUpdate;
}
/**
* Returns \c true if the updater should notify the user when it finishes
* checking for updates.
*
* \note If set to \c true, the \c Updater will notify the user even when there
* are no updates available (by congratulating him/her about being smart)
*/
bool Updater::notifyOnFinish() const
{
return m_notifyOnFinish;
}
/**
* Returns \c true if there the current update is mandatory.
* \warning You should call \c checkForUpdates() before using this function
*/
bool Updater::mandatoryUpdate() const
{
return m_mandatoryUpdate;
}
/**
* Returns \c true if there is an update available.
* \warning You should call \c checkForUpdates() before using this function
*/
bool Updater::updateAvailable() const
{
return m_updateAvailable;
}
/**
* Returns \c true if the integrated downloader is enabled.
* \note If set to \c true, the \c Updater will open the downloader dialog if
* the user agrees to download the update.
*/
bool Updater::downloaderEnabled() const
{
return m_downloaderEnabled;
}
/**
* Returns \c true if the updater shall not intervene when the download has
* finished (you can use the \c QSimpleUpdater signals to know when the
* download is completed).
*/
bool Updater::useCustomInstallProcedures() const
{
return m_downloader->useCustomInstallProcedures();
}
/**
* Downloads and interpets the update definitions file referenced by the
* \c url() function.
*/
void Updater::checkForUpdates()
{
QNetworkRequest request(url());
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
if (!userAgentString().isEmpty())
request.setRawHeader("User-Agent", userAgentString().toUtf8());
m_manager->get(request);
}
/**
* Changes the \c url in which the \c Updater can find the update definitions
* file.
*/
void Updater::setUrl(const QString &url)
{
m_url = url;
}
/**
* Changes the module \a name.
* \note The module name is used on the user prompts. If the module name is
* empty, then the prompts will show the name of the application.
*/
void Updater::setModuleName(const QString &name)
{
m_moduleName = name;
}
/**
* If \a notify is set to \c true, then the \c Updater will notify the user
* when an update is available.
*/
void Updater::setNotifyOnUpdate(const bool notify)
{
m_notifyOnUpdate = notify;
}
/**
* If \a notify is set to \c true, then the \c Updater will notify the user
* when it has finished interpreting the update definitions file.
*/
void Updater::setNotifyOnFinish(const bool notify)
{
m_notifyOnFinish = notify;
}
/**
* Changes the user agent string used to identify the client application
* from the server in a HTTP session.
*
* By default, the user agent will co
*/
void Updater::setUserAgentString(const QString &agent)
{
m_userAgentString = agent;
m_downloader->setUserAgentString(agent);
}
/**
* Changes the module \a version
* \note The module version is used to compare the local and remote versions.
* If the \a version parameter is empty, then the \c Updater will use the
* application version (referenced by \c qApp)
*/
void Updater::setModuleVersion(const QString &version)
{
m_moduleVersion = version;
}
/**
* If the \a enabled parameter is set to \c true, the \c Updater will open the
* integrated downloader if the user agrees to install the update (if any)
*/
void Updater::setDownloaderEnabled(const bool enabled)
{
m_downloaderEnabled = enabled;
}
/**
* Changes the platform key.
* If the platform key is empty, then the system will use the following keys:
* - On iOS: \c ios
* - On Mac OSX: \c osx
* - On Android: \c android
* - On GNU/Linux: \c linux
* - On Microsoft Windows: \c windows
*/
void Updater::setPlatformKey(const QString &platformKey)
{
m_platform = platformKey;
}
/**
* If the \a customAppcast parameter is set to \c true, then the \c Updater
* will not try to read the network reply from the server, instead, it will
* emit the \c appcastDownloaded() signal, which allows the application to
* read and interpret the appcast file by itself
*/
void Updater::setUseCustomAppcast(const bool customAppcast)
{
m_customAppcast = customAppcast;
}
/**
* If the \a custom parameter is set to \c true, the \c Updater will not try
* to open the downloaded file. Use the signals fired by the \c QSimpleUpdater
* to install the update from the downloaded file by yourself.
*/
void Updater::setUseCustomInstallProcedures(const bool custom)
{
m_downloader->setUseCustomInstallProcedures(custom);
}
/**
* If the \a mandatory_update is set to \c true, the \c Updater has to download
* and install the update. If the user cancels or exits, the application will
* close
*/
void Updater::setMandatoryUpdate(const bool mandatory_update)
{
m_mandatoryUpdate = mandatory_update;
}
/**
* Called when the download of the update definitions file is finished.
*/
void Updater::onReply(QNetworkReply *reply)
{
/* Check if we need to redirect */
QUrl redirect
= reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (!redirect.isEmpty())
{
setUrl(redirect.toString());
checkForUpdates();
return;
}
/* There was a network error */
if (reply->error() != QNetworkReply::NoError)
{
setUpdateAvailable(false);
emit checkingFinished(url());
return;
}
/* The application wants to interpret the appcast by itself */
if (customAppcast())
{
emit appcastDownloaded(url(), reply->readAll());
emit checkingFinished(url());
return;
}
/* Try to create a JSON document from downloaded data */
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
/* JSON is invalid */
if (document.isNull())
{
setUpdateAvailable(false);
emit checkingFinished(url());
return;
}
/* Get the platform information */
QJsonObject updates = document.object().value("updates").toObject();
QJsonObject platform = updates.value(platformKey()).toObject();
/* Get update information */
m_openUrl = platform.value("open-url").toString();
m_changelog = platform.value("changelog").toString();
m_downloadUrl = platform.value("download-url").toString();
m_latestVersion = platform.value("latest-version").toString();
if (platform.contains("mandatory-update"))
m_mandatoryUpdate = platform.value("mandatory-update").toBool();
/* Compare latest and current version */
setUpdateAvailable(compare(latestVersion(), moduleVersion()));
emit checkingFinished(url());
}
/**
* Prompts the user based on the value of the \a available parameter and the
* settings of this instance of the \c Updater class.
*/
void Updater::setUpdateAvailable(const bool available)
{
m_updateAvailable = available;
QMessageBox box;
box.setTextFormat(Qt::RichText);
box.setIcon(QMessageBox::Information);
if (updateAvailable() && (notifyOnUpdate() || notifyOnFinish()))
{
QString text = tr("Would you like to download the update now?");
if (m_mandatoryUpdate)
{
text = tr("Would you like to download the update now? This is a "
"mandatory update, exiting now will close the "
"application");
}
QString title = "<h3>"
+ tr("Version %1 of %2 has been released!")
.arg(latestVersion())
.arg(moduleName())
+ "</h3>";
box.setText(title);
box.setInformativeText(text);
box.setStandardButtons(QMessageBox::No | QMessageBox::Yes);
box.setDefaultButton(QMessageBox::Yes);
if (box.exec() == QMessageBox::Yes)
{
if (!openUrl().isEmpty())
QDesktopServices::openUrl(QUrl(openUrl()));
else if (downloaderEnabled())
{
m_downloader->setUrlId(url());
m_downloader->setFileName(downloadUrl().split("/").last());
m_downloader->setMandatoryUpdate(m_mandatoryUpdate);
m_downloader->startDownload(QUrl(downloadUrl()));
}
else
QDesktopServices::openUrl(QUrl(downloadUrl()));
}
else
{
if (m_mandatoryUpdate)
{
QApplication::quit();
}
}
}
else if (notifyOnFinish())
{
box.setStandardButtons(QMessageBox::Close);
box.setInformativeText(tr("No updates are available for the moment"));
box.setText("<h3>"
+ tr("Congratulations! You are running the "
"latest version of %1")
.arg(moduleName())
+ "</h3>");
box.exec();
}
}
/**
* Compares the two version strings (\a x and \a y).
* - If \a x is greater than \y, this function returns \c true.
* - If \a y is greater than \x, this function returns \c false.
* - If both versions are the same, this function returns \c false.
*/
bool Updater::compare(const QString &x, const QString &y)
{
QStringList versionsX = x.split(".");
QStringList versionsY = y.split(".");
int count = qMin(versionsX.count(), versionsY.count());
for (int i = 0; i < count; ++i)
{
int a = QString(versionsX.at(i)).toInt();
int b = QString(versionsY.at(i)).toInt();
if (a > b)
return true;
else if (b > a)
return false;
}
return versionsY.count() < versionsX.count();
}
#if QSU_INCLUDE_MOC
# include "moc_Updater.cpp"
#endif

View File

@ -0,0 +1,113 @@
/*
* Copyright (c) 2014-2021 Alex Spataru <https://github.com/alex-spataru>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef _QSIMPLEUPDATER_UPDATER_H
#define _QSIMPLEUPDATER_UPDATER_H
#include <QUrl>
#include <QObject>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QSimpleUpdater.h>
class Downloader;
/**
* \brief Downloads and interprests the update definition file
*/
class QSU_SHARED_EXPORT Updater : public QObject
{
Q_OBJECT
signals:
void checkingFinished(const QString &url);
void downloadFinished(const QString &url, const QString &filepath);
void appcastDownloaded(const QString &url, const QByteArray &data);
public:
Updater();
~Updater();
QString url() const;
QString openUrl() const;
QString changelog() const;
QString moduleName() const;
QString downloadUrl() const;
QString platformKey() const;
QString moduleVersion() const;
QString latestVersion() const;
QString userAgentString() const;
bool mandatoryUpdate() const;
bool customAppcast() const;
bool notifyOnUpdate() const;
bool notifyOnFinish() const;
bool updateAvailable() const;
bool downloaderEnabled() const;
bool useCustomInstallProcedures() const;
public slots:
void checkForUpdates();
void setUrl(const QString &url);
void setModuleName(const QString &name);
void setNotifyOnUpdate(const bool notify);
void setNotifyOnFinish(const bool notify);
void setUserAgentString(const QString &agent);
void setModuleVersion(const QString &version);
void setDownloaderEnabled(const bool enabled);
void setPlatformKey(const QString &platformKey);
void setUseCustomAppcast(const bool customAppcast);
void setUseCustomInstallProcedures(const bool custom);
void setMandatoryUpdate(const bool mandatory_update);
private slots:
void onReply(QNetworkReply *reply);
void setUpdateAvailable(const bool available);
private:
bool compare(const QString &x, const QString &y);
private:
QString m_url;
QString m_userAgentString;
bool m_customAppcast;
bool m_notifyOnUpdate;
bool m_notifyOnFinish;
bool m_updateAvailable;
bool m_downloaderEnabled;
bool m_mandatoryUpdate;
QString m_openUrl;
QString m_platform;
QString m_changelog;
QString m_moduleName;
QString m_downloadUrl;
QString m_moduleVersion;
QString m_latestVersion;
Downloader *m_downloader;
QNetworkAccessManager *m_manager;
};
#endif

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2015-2016 Alex Spataru <alex_spataru@outlook.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef TEST_DOWNLOADER_H
#define TEST_DOWNLOADER_H
#include <QtTest>
#include <Downloader.h>
class Test_Downloader : public QObject
{
Q_OBJECT
};
#endif

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2015-2016 Alex Spataru <alex_spataru@outlook.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef TEST_QSIMPLEUPDATER_H
#define TEST_QSIMPLEUPDATER_H
#include <QtTest>
#include <QSimpleUpdater.h>
class Test_QSimpleUpdater : public QObject
{
Q_OBJECT
};
#endif

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2015-2016 Alex Spataru <alex_spataru@outlook.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef TEST_UPDATER_H
#define TEST_UPDATER_H
#include <QtTest>
#include <Updater.h>
class Test_Updater : public QObject
{
Q_OBJECT
};
#endif

View File

@ -0,0 +1,36 @@
#
# Copyright (c) 2016 Alex Spataru <alex_spataru@outlook.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
QT += testlib
TARGET = QSimpleUpdater_Test
include ($$PWD/../QSimpleUpdater.pri)
INCLUDEPATH += $$PWD/../src
SOURCES += \
$$PWD/main.cpp
HEADERS += \
$$PWD/Test_Downloader.h \
$$PWD/Test_QSimpleUpdater.h \
$$PWD/Test_Updater.h

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2015-2016 Alex Spataru <alex_spataru@outlook.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "Test_Updater.h"
#include "Test_Downloader.h"
#include "Test_QSimpleUpdater.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setApplicationName("QSimpleUpdater Tests");
app.setOrganizationName("The QSimpleUpdater Library");
QTest::qExec(new Test_Updater, argc, argv);
QTest::qExec(new Test_Downloader, argc, argv);
QTest::qExec(new Test_QSimpleUpdater, argc, argv);
QTimer::singleShot(1000, Qt::PreciseTimer, qApp, SLOT(quit()));
return app.exec();
}

View File

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

View File

@ -0,0 +1,12 @@
# What is this?
The [updates.json](updates.json) file is the update definitions file. In other words, it lists the latest versions, changelogs and download URLs for each platform.
The example project downloads the [updates.json](updates.json) file and analyzes in order to:
- Compare the local (user set) version and the remote version (specified in [updates.json](updates.json))
- Download the change log for the current platform
- Obtain the download URLs
- Obtain the URL to open (if specified)
Check the article on this article on the [wiki](#) for more information.

View File

@ -0,0 +1,39 @@
{
"updates": {
"windows": {
"open-url": "",
"latest-version": "1.0",
"download-url": "https://raw.githubusercontent.com/alex-spataru/QSimpleUpdater/master/tutorial/download/YesItWorks.jpg",
"changelog": "This is an example changelog for Windows. Go on...",
"mandatory": true
},
"osx": {
"open-url": "",
"latest-version": "1.0",
"download-url": "https://raw.githubusercontent.com/alex-spataru/QSimpleUpdater/master/tutorial/download/YesItWorks.jpg",
"changelog": "This is an example changelog for Mac OS X. Go on...",
"mandatory": true
},
"linux": {
"open-url": "",
"latest-version": "1.0",
"download-url": "https://raw.githubusercontent.com/alex-spataru/QSimpleUpdater/master/tutorial/download/YesItWorks.jpg",
"changelog": "This is an example changelog for Linux. Go on...",
"mandatory": true
},
"ios": {
"open-url": "",
"latest-version": "1.0",
"download-url": "https://raw.githubusercontent.com/alex-spataru/QSimpleUpdater/master/tutorial/download/YesItWorks.jpg",
"changelog": "This is an example changelog for iOS. Go on...",
"mandatory": true
},
"android": {
"open-url": "",
"latest-version": "1.0",
"download-url": "https://raw.githubusercontent.com/alex-spataru/QSimpleUpdater/master/tutorial/download/YesItWorks.jpg",
"changelog": "This is an example changelog for Android. Go on...",
"mandatory": true
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -0,0 +1,134 @@
/*
* Copyright (c) 2014-2016 Alex Spataru <alex_spataru@outlook.com>
*
* This work is free. You can redistribute it and/or modify it under the
* terms of the Do What The Fuck You Want To Public License, Version 2,
* as published by Sam Hocevar. See the COPYING file for more details.
*/
#include "Window.h"
#include "ui_Window.h"
#include <QDebug>
#include <QSimpleUpdater.h>
//==============================================================================
// Define the URL of the Update Definitions file
//==============================================================================
static const QString DEFS_URL = "https://raw.githubusercontent.com/"
"alex-spataru/QSimpleUpdater/master/tutorial/"
"definitions/updates.json";
//==============================================================================
// Window::Window
//==============================================================================
Window::Window(QWidget *parent)
: QMainWindow(parent)
{
m_ui = new Ui::Window;
m_ui->setupUi(this);
setWindowTitle(qApp->applicationName());
/* QSimpleUpdater is single-instance */
m_updater = QSimpleUpdater::getInstance();
/* Check for updates when the "Check For Updates" button is clicked */
connect(m_updater, SIGNAL(checkingFinished(QString)), this,
SLOT(updateChangelog(QString)));
connect(m_updater, SIGNAL(appcastDownloaded(QString, QByteArray)), this,
SLOT(displayAppcast(QString, QByteArray)));
/* React to button clicks */
connect(m_ui->resetButton, SIGNAL(clicked()), this, SLOT(resetFields()));
connect(m_ui->closeButton, SIGNAL(clicked()), this, SLOT(close()));
connect(m_ui->checkButton, SIGNAL(clicked()), this, SLOT(checkForUpdates()));
/* Resize the dialog to fit */
setMinimumSize(minimumSizeHint());
resize(minimumSizeHint());
/* Reset the UI state */
resetFields();
}
//==============================================================================
// Window::~Window
//==============================================================================
Window::~Window()
{
delete m_ui;
}
//==============================================================================
// Window::checkForUpdates
//==============================================================================
void Window::resetFields()
{
m_ui->installedVersion->setText("0.1");
m_ui->customAppcast->setChecked(false);
m_ui->enableDownloader->setChecked(true);
m_ui->showAllNotifcations->setChecked(false);
m_ui->showUpdateNotifications->setChecked(true);
m_ui->mandatoryUpdate->setChecked(false);
}
//==============================================================================
// Window::checkForUpdates
//==============================================================================
void Window::checkForUpdates()
{
/* Get settings from the UI */
QString version = m_ui->installedVersion->text();
bool customAppcast = m_ui->customAppcast->isChecked();
bool downloaderEnabled = m_ui->enableDownloader->isChecked();
bool notifyOnFinish = m_ui->showAllNotifcations->isChecked();
bool notifyOnUpdate = m_ui->showUpdateNotifications->isChecked();
bool mandatoryUpdate = m_ui->mandatoryUpdate->isChecked();
/* Apply the settings */
m_updater->setModuleVersion(DEFS_URL, version);
m_updater->setNotifyOnFinish(DEFS_URL, notifyOnFinish);
m_updater->setNotifyOnUpdate(DEFS_URL, notifyOnUpdate);
m_updater->setUseCustomAppcast(DEFS_URL, customAppcast);
m_updater->setDownloaderEnabled(DEFS_URL, downloaderEnabled);
m_updater->setMandatoryUpdate(DEFS_URL, mandatoryUpdate);
/* Check for updates */
m_updater->checkForUpdates(DEFS_URL);
}
//==============================================================================
// Window::updateChangelog
//==============================================================================
void Window::updateChangelog(const QString &url)
{
if (url == DEFS_URL)
m_ui->changelogText->setText(m_updater->getChangelog(url));
}
//==============================================================================
// Window::displayAppcast
//==============================================================================
void Window::displayAppcast(const QString &url, const QByteArray &reply)
{
if (url == DEFS_URL)
{
QString text
= "This is the downloaded appcast: <p><pre>" + QString::fromUtf8(reply)
+ "</pre></p><p> If you need to store more information on the "
"appcast (or use another format), just use the "
"<b>QSimpleUpdater::setCustomAppcast()</b> function. "
"It allows your application to interpret the appcast "
"using your code and not QSU's code.</p>";
m_ui->changelogText->setText(text);
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2014-2016 Alex Spataru <alex_spataru@outlook.com>
*
* This work is free. You can redistribute it and/or modify it under the
* terms of the Do What The Fuck You Want To Public License, Version 2,
* as published by Sam Hocevar. See the COPYING file for more details.
*/
#ifndef _WINDOW_H
#define _WINDOW_H
#include <QMainWindow>
#include <QApplication>
namespace Ui
{
class Window;
}
class QSimpleUpdater;
class Window : public QMainWindow
{
Q_OBJECT
public:
explicit Window(QWidget *parent = 0);
~Window();
public slots:
void resetFields();
void checkForUpdates();
void updateChangelog(const QString &url);
void displayAppcast(const QString &url, const QByteArray &reply);
private:
Ui::Window *m_ui;
QSimpleUpdater *m_updater;
};
#endif

View File

@ -0,0 +1,222 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Window</class>
<widget class="QMainWindow" name="Window">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>483</width>
<height>494</height>
</rect>
</property>
<property name="windowTitle">
<string>QSimpleUpdater Example</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../etc/resources/qsimpleupdater.qrc">:/icons/update.png</pixmap>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_3">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>&lt;h1&gt;&lt;i&gt;QSimpleUpdater&lt;/i&gt;&lt;/h1&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>&lt;i&gt;A simpler way to update your Qt applications...&lt;/i&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Updater Options</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="1">
<widget class="QLineEdit" name="installedVersion">
<property name="text">
<string>0.1</string>
</property>
<property name="placeholderText">
<string>Write a version string...</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Set installed version (latest version is 1.0)</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="customAppcast">
<property name="text">
<string>Do not use the QSU library to read the appcast</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="showUpdateNotifications">
<property name="text">
<string>Notify me when an update is available</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="showAllNotifcations">
<property name="text">
<string>Show all notifications</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="enableDownloader">
<property name="text">
<string>Enable integrated downloader</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="mandatoryUpdate">
<property name="text">
<string>Mandatory Update</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Changelog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTextBrowser" name="changelogText">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'.Lucida Grande UI'; font-size:13pt;&quot;&gt;Click &amp;quot;Check for Updates&amp;quot; to update this field...&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="resetButton">
<property name="text">
<string>Reset Fields</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="checkButton">
<property name="text">
<string>Check for Updates</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<resources>
<include location="../../etc/resources/qsimpleupdater.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2014-2016 Alex Spataru <alex_spataru@outlook.com>
*
* This work is free. You can redistribute it and/or modify it under the
* terms of the Do What The Fuck You Want To Public License, Version 2,
* as published by Sam Hocevar. See the COPYING file for more details.
*/
#include "Window.h"
int main(int argc, char **argv)
{
QApplication app(argc, argv);
app.setApplicationVersion("1.0");
app.setApplicationName("Bob's Badass App");
Window window;
window.show();
return app.exec();
}

View File

@ -0,0 +1,18 @@
#
# Copyright (c) 2014-2016 Alex Spataru <alex_spataru@outlook.com>
#
# This work is free. You can redistribute it and/or modify it under the
# terms of the Do What The Fuck You Want To Public License, Version 2,
# as published by Sam Hocevar. See the COPYING file for more details.
#
TEMPLATE = app
TARGET = "QSU Tutorial"
FORMS += $$PWD/src/Window.ui
HEADERS += $$PWD/src/Window.h
SOURCES += $$PWD/src/Window.cpp \
$$PWD/src/main.cpp
include ($$PWD/../QSimpleUpdater.pri)

@ -1 +0,0 @@
Subproject commit bfd4a908dc4cc3f92dda031bbd3572e653019483

38
lib/qtcsv/.clang-format Normal file
View File

@ -0,0 +1,38 @@
BasedOnStyle: LLVM
Standard: Cpp11
CommentPragmas: "^!|^:"
PointerBindsToType: false
SpaceAfterTemplateKeyword: false
BreakBeforeBinaryOperators: All
BreakBeforeBraces: Custom
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeComma
ConstructorInitializerIndentWidth: 2
NamespaceIndentation: None
AlignAfterOpenBracket: true
AlwaysBreakTemplateDeclarations: true
AllowShortFunctionsOnASingleLine: Inline
SortIncludes: false
IndentCaseLabels: true
IndentPPDirectives: AfterHash
AccessModifierOffset: -2
IndentWidth: 2
ColumnLimit: 80
BraceWrapping:
AfterClass: true
AfterControlStatement: true
AfterEnum: true
AfterFunction: true
AfterNamespace: true
AfterObjCDeclaration: true
AfterStruct: true
AfterUnion: true
BeforeCatch: true
BeforeElse: true
IndentBraces: false
ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE ]
#StatementMacros ['Q_OBJECT', 'Q_UNUSED']

106
lib/qtcsv/CMakeLists.txt Normal file
View File

@ -0,0 +1,106 @@
#
# Copyright (c) 2024 Alex Spataru <https://github.com/alex-spataru>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#-------------------------------------------------------------------------------
# Project setup
#-------------------------------------------------------------------------------
cmake_minimum_required(VERSION 3.19)
project(qtcsv CXX)
#-------------------------------------------------------------------------------
# C++ options
#-------------------------------------------------------------------------------
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
#-------------------------------------------------------------------------------
# Add external dependencies
#-------------------------------------------------------------------------------
find_package(Qt6 COMPONENTS Core REQUIRED)
#-------------------------------------------------------------------------------
# Import source code & resources
#-------------------------------------------------------------------------------
include_directories(include)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
set(SOURCES
sources/writer.cpp
sources/reader.cpp
sources/variantdata.cpp
sources/stringdata.cpp
sources/contentiterator.cpp
)
set(HEADERS
sources/symbols.h
sources/filechecker.h
sources/contentiterator.h
include/qtcsv/writer.h
include/qtcsv/reader.h
include/qtcsv/stringdata.h
include/qtcsv/variantdata.h
include/qtcsv/qtcsv_global.h
include/qtcsv/abstractdata.h
)
#-------------------------------------------------------------------------------
# Compile & link the library
#-------------------------------------------------------------------------------
add_library(
qtcsv
STATIC
${SOURCES}
${HEADERS}
)
target_link_libraries(
qtcsv PUBLIC
Qt6::Core
)
target_include_directories(
qtcsv PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
#-------------------------------------------------------------------------------
# Configure unity build
#-------------------------------------------------------------------------------
set_target_properties(
qtcsv PROPERTIES
UNITY_BUILD ON
UNITY_BUILD_MODE BATCH
UNITY_BUILD_BATCH_SIZE 128
INTERPROCEDURAL_OPTIMIZATION TRUE
)

22
lib/qtcsv/LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Antony Cherepanov (antony.cherepanov@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

475
lib/qtcsv/README.md Normal file
View File

@ -0,0 +1,475 @@
# qtcsv
[![Build status](https://ci.appveyor.com/api/projects/status/7uv7ghs9uexf08bv/branch/master?svg=true)](https://ci.appveyor.com/project/iamantony/qtcsv/branch/master)
Small easy-to-use library for reading and writing [csv-files][csvwiki] in Qt.
Qt suppport:
- Qt6: branch `master` (you're here)
- Qt4 and Qt5: branch `qt4_qt5`
Tested on:
- Ubuntu with gcc, Qt6
- Windows with MinGW, Qt6
- OS X with clang, Qt6
## Table of contents
* [1. Quick Example](#1-quick-example)
* [2. Usage](#2-usage)
* [2.1 Containers](#21-containers)
* [2.1.1 AbstractData](#211-abstractdata)
* [2.1.2 StringData](#212-stringdata)
* [2.1.3 VariantData](#213-variantdata)
* [2.2 Reader](#22-reader)
* [2.2.1 Reader functions](#221-reader-functions)
* [2.2.2 AbstractProcessor](#222-abstractprocessor)
* [2.3 Writer](#23-writer)
* [3. Requirements](#3-requirements)
* [4. Build](#4-build)
* [4.1 Building on Linux, OS X](#41-building-on-linux-os-x)
* [4.1.1 Using qmake](#411-using-qmake)
* [4.1.2 Using cmake](#412-using-cmake)
* [4.2 Building on Windows](#42-building-on-windows)
* [4.2.1 Prebuild step on Windows](#421-prebuild-step-on-windows)
* [4.2.2 Using qmake](#422-using-qmake)
* [4.2.3 Using cmake](#423-using-cmake)
* [5. Run tests](#5-run-tests)
* [5.1 Linux, OS X](#51-linux-os-x)
* [5.2 Windows](#52-windows)
* [6. Installation](#6-installation)
* [7. Examples](#7-examples)
* [8. Other](#8-other)
* [9. Creators](#9-creators)
## 1. Quick Example
```cpp
#include <QList>
#include <QString>
#include <QDir>
#include <QDebug>
#include "qtcsv/stringdata.h"
#include "qtcsv/reader.h"
#include "qtcsv/writer.h"
int main()
{
// prepare data that you want to save to csv-file
QList<QString> strList;
strList << "one" << "two" << "three";
QtCSV::StringData strData;
strData.addRow(strList);
strData.addEmptyRow();
strData << strList << "this is the last row";
// write to file
const auto filePath = QDir::currentPath() + "/test.csv";
QtCSV::Writer::write(filePath, strData);
// read data from file
const auto readData = QtCSV::Reader::readToList(filePath);
for (auto i = 0; i < readData.size(); ++i)
{
qDebug() << readData.at(i).join(",");
}
return 0;
}
```
## 2. Usage
Library could be separated into three parts: **_Reader_**,
**_Writer_** and **_Containers_**.
### 2.1 Containers
*qtcsv* library can work with standard Qt containers (like QList) and special data containers.
#### 2.1.1 AbstractData
**[_AbstractData_][absdata]** is a pure abstract class that provides
interface for a class of special data containers.
```cpp
class AbstractData {
public:
virtual ~AbstractData() = default;
virtual void addEmptyRow() = 0;
virtual void addRow(const QList<QString>& values) = 0;
virtual void clear() = 0;
virtual bool isEmpty() const = 0;
virtual qsizetype rowCount() const = 0;
virtual QList<QString> rowValues(qsizetype row) const = 0;
};
```
As you can see, **_AbstractData_** declare virtual functions for adding new rows,
getting rows values, clearing all information and so on. Basic stuff for a
container class.
#### 2.1.2 StringData
**[_StringData_][strdata]** inhertis interface of **_AbstractData_**
class and provides some useful functions for inserting/removing rows and
so on. Class uses strings to store data.
#### 2.1.3 VariantData
If you store information in different types - integers, floating point
values, strings or (almost) anything else (example: [1, 3.14, "check"]) -
and you don't want to manually transform each element to string, then you
can use **_QVariant_** magic. Wrap your data into **_QVariants_** and pass it to
**[_VariantData_][vardata]** class. It also inherits interface of **_AbstractData_**
plus has several useful methods.
### 2.2 Reader
Use **[_Reader_][reader]** class to read csv-files / csv-data. Let's see it's functions.
#### 2.2.1 Reader functions
1. Read data to **_QList\<QList\<QString\>\>_**
```cpp
QList<QList<QString>> readToList(
const QString& filePath,
const QString& separator = QString(","),
const QString& textDelimiter = QString("\""),
QStringConverter::Encoding codec = QStringConverter::Utf8);
QList<QList<QString>> readToList(
QIODevice& ioDevice,
const QString& separator = QString(","),
const QString& textDelimiter = QString("\""),
QStringConverter::Encoding codec = QStringConverter::Utf8);
```
- *filePath* - string with absolute path to existent csv-file
(example: "/home/user/my-file.csv");
- *ioDevice* - IO Device that contains csv-formatted data;
- *separator* (optional) - delimiter symbol, that separates elements
in a row (by default it is comma - ",");
- *textDelimiter* (optional) - text delimiter symbol that encloses
each element in a row (by default it is double quoute - ");
- *codec* (optional) - codec type that will be used
to read data from the file (by default it is UTF-8 codec).
As a result function will return **_QList\<QList\<QString\>\>_** that holds content
of the file / IO Device. Size of it will be equal to the number of rows
in csv-data source. Each **_QList\<QString\>_** will contain elements of the
corresponding row. On error these functions will return empty list.
2. Read data to **_AbstractData_**-based container
```cpp
bool readToData(
const QString& filePath,
AbstractData& data,
const QString& separator = QString(","),
const QString& textDelimiter = QString("\""),
QStringConverter::Encoding codec = QStringConverter::Utf8);
bool readToData(
QIODevice& ioDevice,
AbstractData& data,
const QString& separator = QString(","),
const QString& textDelimiter = QString("\""),
QStringConverter::Encoding codec = QStringConverter::Utf8);
```
These functions are little more advanced and, I hope, a little more useful.
- *filePath* - string with absolute path to existent csv-file;
- *ioDevice* - IO Device that contains csv-formatted data;
- *data* - reference to **_AbstractData_**-based class object;
- *separator* (optional) - delimiter symbol;
- *textDelimiter* (optional) - text delimiter symbol;
- *codec* (optional) - codec type.
Functions will save content of the file / IO Device in *data* object using virtual
function **_AbstractData::addRow(QList\<QString\>)_**. Elements of csv-data will be
saved as strings in objects of **_StringData_** / **_VariantData_**.
If you would like to convert row elements to the target types on-the-fly during
file reading, please implement your own **_AbstractData_**-based container class.
3. Read data and process it line-by-line by **_AbstractProcessor_**-based processor
```cpp
bool readToProcessor(
const QString& filePath,
AbstractProcessor& processor,
const QString& separator = QString(","),
const QString& textDelimiter = QString("\""),
QStringConverter::Encoding codec = QStringConverter::Utf8);
bool readToProcessor(
QIODevice& ioDevice,
AbstractProcessor& processor,
const QString& separator = QString(","),
const QString& textDelimiter = QString("\""),
QStringConverter::Encoding codec = QStringConverter::Utf8);
```
- *filePath* - string with absolute path to existent csv-file;
- *ioDevice* - IO Device that contains csv-formatted data;
- *processor* - reference to **_AbstractProcessor_**-based class object;
- *separator* (optional) - delimiter symbol;
- *textDelimiter* (optional) - text delimiter symbol;
- *codec* (optional) - codec type.
This function will read csv-data from file / IO Device line-by-line and
pass data to *processor* object.
#### 2.2.2 AbstractProcessor
**[_AbstractProcessor_][reader]** is an abstract class with two methods:
``` cpp
class AbstractProcessor
{
public:
virtual ~AbstractProcessor() = default;
virtual void preProcessRawLine(QString& /*editable_line*/) {}
virtual bool processRowElements(const QList<QString>& elements) = 0;
};
```
When **_Reader_** opens a csv-data source (file or IO Device), it starts
reading it line by line in a cycle. First of all, **_Reader_** passes each
new line to processor's method **_preProcessRawLine(QString&)_**. In this method
you can edit the line - replace values, remove sensitive information and so on.
After that **_Reader_** parses elements of the row and passes them to processor's
method **_processRowElements(QList\<QString\>)_**. At that step you can do whatever
you want with row elements - convert/edit/save/filter the elements. Please check out
**_ReadToListProcessor_** class (defined in [reader.cpp][reader-cpp]) as an example of
such processor.
### 2.3 Writer
Use **[_Writer_][writer]** class to write csv-data to files / IO Devices.
```cpp
bool write(
const QString& filePath,
const AbstractData& data,
const QString& separator = QString(","),
const QString& textDelimiter = QString("\""),
WriteMode mode = WriteMode::REWRITE,
const QList<QString>& header = {},
const QList<QString>& footer = {},
QStringConverter::Encoding codec = QStringConverter::Utf8);
bool write(
QIODevice& ioDevice,
const AbstractData& data,
const QString& separator = QString(","),
const QString& textDelimiter = QString("\""),
const QList<QString>& header = {},
const QList<QString>& footer = {},
QStringConverter::Encoding codec = QStringConverter::Utf8);
```
- *filePath* - string with absolute path to csv-file (new or existent);
- *ioDevice* - IO Device;
- *data* - object, that contains information that you want to write to
csv-file / IO Device;
- *separator* (optional) - delimiter symbol (by default it is comma - ",");
- *textDelimiter* (optional) - text delimiter symbol that encloses
each element in a row (by default it is double quoute - ");
- *mode* (optional) - write mode flag.
If it set to **_WriteMode::REWRITE_** and csv-file exist, then csv-file will be
rewritten. If *mode* set to **_WriteMode::APPEND_** and csv-file exist, then new
information will be appended to the end of the file. By default mode is set
to **_WriteMode::REWRITE_**.
- *header* (optional) - strings that will be written as the first row;
- *footer* (optional) - strings that will be written at the last row;
- *codec* (optional) - codec type that will be used
in write operations (by default it is UTF-8 codec).
**_Writer_** uses *CRLF* as line ending symbols in accordance with [standard][rfc].
If element of the row contains separator symbol or line ending symbols, such
element will be enclosed by text delimiter symbols (or double quoute if you have set
empty string as text delimiter symbol).
## 3. Requirements
Qt6, only core/base modules.
## 4. Build
### 4.1 Building on Linux, OS X
#### 4.1.1 Using qmake
```bash
cd /path/to/folder/with/qtcsv
# Create build directory
mkdir ./build
cd ./build
# Build library. You can choose build type: release or debug
qmake ../qtcsv.pro CONFIG+=[release|debug]
make
# Create build directory for tests
mkdir ./tests
cd ./tests
# Build tests. Besides of setting build type, we set path where linker could find compiled library file.
qmake ../../tests/tests.pro CONFIG+=[release|debug] LIBS+=-L../
make
```
#### 4.1.2 Using cmake
```bash
cd /path/to/folder/with/qtcsv
# Create build directory
mkdir ./build
cd ./build
# Build library and tests. See CMakeLists.txt for list of additional options that you can set.
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON ..
make
```
### 4.2 Building on Windows
#### 4.2.1 Prebuild step on Windows
If you going to build *qtcsv* library on Windows with MinGW, first of all [check that your PATH variable][path_var] contains paths to _Qt_ and _MinGW_ toolsets.
#### 4.2.2 Using qmake
```bash
cd C:\path\to\folder\with\qtcsv
# Create build directory
mkdir .\build
cd .\build
# Build library. You can choose build type: release or debug. Set DESTDIR to current directory.
qmake ..\qtcsv.pro CONFIG+=[release|debug] DESTDIR=%cd%
mingw32-make
# Create build directory for tests
mkdir .\tests
cd .\tests
# Copy library file into 'tests' directory
copy ..\qtcsv.dll .\
# Build tests
qmake ..\..\tests\tests.pro CONFIG+=[release|debug] DESTDIR=%cd%
mingw32-make
```
#### 4.2.3 Using cmake
```bash
cd C:\path\to\folder\with\qtcsv
# Create build directory
mkdir .\build
cd .\build
# Build library and tests. See CMakeLists.txt for list of additional options that you can set.
cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON ..
mingw32-make
```
## 5. Run tests
To run tests use these commands after build of *qtcsv*:
### 5.1 Linux, OS X
```bash
cd /path/to/folder/with/qtcsv/build/tests
# Set LD_LIBRARY_PATH variable so test binary will know where to search library file.
# Suppose, that library file is located in "build" directory, up a level from current directory.
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/../
chmod 777 qtcsv_tests
./qtcsv_tests
```
### 5.2 Windows
```bash
cd /path/to/folder/with/qtcsv/build/tests
# Copy library file into "tests" directory
copy ..\*qtcsv.dll .\
qtcsv_tests.exe
```
## 6. Installation
On Unix-like OS you can install *qtcsv* library using these commands:
```bash
sudo make install
sudo ldconfig -n -v /usr/local/lib
```
These commands will copy all compiled files (libqtcsv.so\*) from build
folder to *"/usr/local/lib"*. Also all headers files will be copied
from *"./include"* folder to *"/usr/local/include/"*.
All installation settings are defined in [*qtcsv.pro*][qtcsv-pro] file.
See *copy_lib_headers* and *target* variables.
For additional information, see [Qt documentation][install-files] about
files installation.
## 7. Examples
If you would like to try *qtcsv*, you can download [qtcsv-example project][qtcsv-example].
Don't forget to read README.md file!
## 8. Other
If you want to know more about csv-file format, please read [RFC 4180][rfc] standard.
Also on [this page][csvlint] you can find useful tips about how should look
proper csv-file.
## 9. Creators
Author: [Antony Cherepanov][mypage] (antony.cherepanov@gmail.com)
Contributors: [Patrizio "pbek" Bekerle][pbek], [Furkan "Furkanzmc" Üzümcü][Furkanzmc], [Martin "schulmar" Schulze][schulmar], [cguentherTUChemnitz][cguentherTUChemnitz], [David Jung][David_Jung], [Nicu Tofan][TNick], [Florian Apolloner][apollo13], [Michael Pollind][pollend], [Kuba Ober][KubaO], [Akram Abdeslem Chaima][gakramx], [Bogdan Cristea][cristeab], [Markus Krause][markusdd]
[csvwiki]: http://en.wikipedia.org/wiki/Comma-separated_values
[reader]: https://github.com/iamantony/qtcsv/blob/master/include/qtcsv/reader.h
[reader-cpp]: https://github.com/iamantony/qtcsv/blob/master/sources/reader.cpp
[writer]: https://github.com/iamantony/qtcsv/blob/master/include/qtcsv/writer.h
[absdata]: https://github.com/iamantony/qtcsv/blob/master/include/qtcsv/abstractdata.h
[strdata]: https://github.com/iamantony/qtcsv/blob/master/include/qtcsv/stringdata.h
[vardata]: https://github.com/iamantony/qtcsv/blob/master/include/qtcsv/variantdata.h
[qtcsv-pro]: https://github.com/iamantony/qtcsv/blob/master/qtcsv.pro
[install-files]: https://doc.qt.io/qt-6/qmake-advanced-usage.html#installing-files
[qtcsv-example]: https://github.com/iamantony/qtcsv-example
[rfc]: http://tools.ietf.org/pdf/rfc4180.pdf
[path_var]: http://superuser.com/questions/284342/what-are-path-and-other-environment-variables-and-how-can-i-set-or-use-them
[csvlint]: http://csvlint.io/about
[mypage]: https://github.com/iamantony
[pbek]: https://github.com/pbek
[Furkanzmc]: https://github.com/Furkanzmc
[schulmar]: https://github.com/schulmar
[cguentherTUChemnitz]: https://github.com/cguentherTUChemnitz
[David_Jung]: https://github.com/davidljung
[TNick]: https://github.com/TNick
[apollo13]: https://github.com/apollo13
[pollend]: https://github.com/pollend
[KubaO]: https://github.com/KubaO
[gakramx]: https://github.com/gakramx
[cristeab]: https://github.com/cristeab
[markusdd]: https://github.com/markusdd

58
lib/qtcsv/appveyor.yml Normal file
View File

@ -0,0 +1,58 @@
version: "{build}"
init:
- git config --global core.autocrlf input
environment:
matrix:
- QT6: /Users/appveyor/Qt/6.0.0/clang_64
APPVEYOR_BUILD_WORKER_IMAGE: macos
- QT6: /home/appveyor/Qt/6.0.0/gcc_64
APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
- QT6: C:\Qt\6.0.1\mingw81_64
MINGW: C:\Qt\Tools\mingw810_64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
matrix:
fast_finish: false
for:
- matrix:
only:
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
- APPVEYOR_BUILD_WORKER_IMAGE: macos
build_script:
- echo $APPVEYOR_BUILD_WORKER_IMAGE build script
- "echo Shell: $SHELL"
- "echo Home: $HOME"
- export PATH=$QT6/bin:$PATH
- export CMAKE_PREFIX_PATH=$QT6
- ls -lah $QT6
- ls -lah $QT6/bin
- mkdir build
- cd build
- cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release --build ..
- make
- cd tests
- ./qtcsv_tests
- matrix:
only:
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
build_script:
- ps: echo Windows build script
- ps: echo $env:PATH
- ps: (dir env:)
- ps: $env:PATH="$env:MINGW\bin;$env:QT6\bin;$env:PATH"
- ps: $env:QT_LOGGING_RULES="*.debug=true"
- ps: mkdir build
- ps: cd build
# build library and tests
- ps: cmake --version
# IGNORE_PATH is a workaround for CMake not wanting sh.exe on PATH for MinGW
- ps: cmake -G "MinGW Makefiles" -DCMAKE_IGNORE_PATH="C:/Program Files/Git/usr/bin" -DCMAKE_BUILD_TYPE=Release ..
- ps: ls -s
- ps: mingw32-make
- ps: cd tests
- ps: bash.exe -c "cp ../libqtcsv.dll .; ./qtcsv_tests.exe"

View File

@ -0,0 +1,43 @@
#ifndef QTCSVABSTRACTDATA_H
#define QTCSVABSTRACTDATA_H
#include "qtcsv_global.h"
namespace QtCSV
{
// AbstractData is a pure abstract container class. Its main purpouse is to
// provide common interface for concrete container classes that could be
// used in processing of csv-files.
//
// You can create concrete Data class with AbstractData as public base class
// and implement functions for:
// - adding new rows of values;
// - getting rows values;
// - clearing all saved information;
// - and so on.
//
// Note, that AbstractData is just an interface for container class, not a
// container class. So you are free to decide how to store
// information in derived classes.
class QTCSVSHARED_EXPORT AbstractData
{
public:
virtual ~AbstractData() = default;
// Add new empty row
virtual void addEmptyRow() = 0;
// Add new row with specified values
virtual void addRow(const QList<QString> &values) = 0;
// Clear all data
virtual void clear() = 0;
// Check if there are any rows
virtual bool isEmpty() const = 0;
// Get number of rows
virtual qsizetype rowCount() const = 0;
// Get values of specified row as list of strings
virtual QList<QString> rowValues(qsizetype row) const = 0;
};
} // namespace QtCSV
#endif // QTCSVABSTRACTDATA_H

View File

@ -0,0 +1,14 @@
#ifndef QTCSV_GLOBAL_H
#define QTCSV_GLOBAL_H
#include <QtGlobal>
#if defined(QTCSV_BUILD_DLIB)
# define QTCSVSHARED_EXPORT Q_DECL_EXPORT
#elif defined(QTCSV_USE_DLIB)
# define QTCSVSHARED_EXPORT Q_DECL_IMPORT
#else
# define QTCSVSHARED_EXPORT
#endif
#endif // QTCSV_GLOBAL_H

View File

@ -0,0 +1,101 @@
#ifndef QTCSVREADER_H
#define QTCSVREADER_H
#include "qtcsv/qtcsv_global.h"
#include "abstractdata.h"
#include <QIODevice>
#include <QList>
#include <QString>
#include <QStringConverter>
namespace QtCSV
{
// Reader class is a file reader that work with csv-files. It needs an
// absolute path to the csv-file that you are going to read or
// some IO Device with csv-formatted data.
//
// Additionally you can specify:
// - a separator character (or string) that is used as separator of row
// values. Default separator is comma (",");
// - text delimiter character (or string) that encloses each element in a
// row. Typical delimiter characters: none (""), quote ("'")
// and double quotes ("\"");
// - text codec.
//
// Reader can save (or transfer) information to:
// - QList<QList<QString>>, where each QList<QString> contains values
// of one row;
// - AbstractData-based container class;
// - AbstractProcessor-based object.
class QTCSVSHARED_EXPORT Reader
{
public:
// AbstractProcessor is a class that could be used to process csv-data
// line by line
class QTCSVSHARED_EXPORT AbstractProcessor
{
public:
virtual ~AbstractProcessor() = default;
// Preprocess one raw line from a file
// @input:
// editable_line - raw line from a file
virtual void preProcessRawLine(QString & /*editable_line*/) {}
// Process one row worth of elements
// @input:
// - elements - list of row elements
// @output:
// bool - True if elements was processed successfully, False in case
// of error. If process() return False, the csv-file will be stopped
// reading
virtual bool processRowElements(const QList<QString> &elements) = 0;
};
// Read csv-file and save it's data as strings to QList<QList<QString>>
static QList<QList<QString>>
readToList(const QString &filePath, const QString &separator = QString(","),
const QString &textDelimiter = QString("\""),
QStringConverter::Encoding codec = QStringConverter::Utf8);
// Read csv-formatted data from IO Device and save it
// as strings to QList<QList<QString>>
static QList<QList<QString>>
readToList(QIODevice &ioDevice, const QString &separator = QString(","),
const QString &textDelimiter = QString("\""),
QStringConverter::Encoding codec = QStringConverter::Utf8);
// Read csv-file and save it's data to AbstractData-based container
// class
static bool readToData(const QString &filePath, AbstractData &data,
const QString &separator = QString(","),
const QString &textDelimiter = QString("\""),
QStringConverter::Encoding codec
= QStringConverter::Utf8);
// Read csv-formatted data from IO Device and save it
// to AbstractData-based container class
static bool readToData(QIODevice &ioDevice, AbstractData &data,
const QString &separator = QString(","),
const QString &textDelimiter = QString("\""),
QStringConverter::Encoding codec
= QStringConverter::Utf8);
// Read csv-file and process it line-by-line
static bool
readToProcessor(const QString &filePath, AbstractProcessor &processor,
const QString &separator = QString(","),
const QString &textDelimiter = QString("\""),
QStringConverter::Encoding codec = QStringConverter::Utf8);
// Read csv-formatted data from IO Device and process it line-by-line
static bool readToProcessor(QIODevice &ioDevice, AbstractProcessor &processor,
const QString &separator = QString(","),
const QString &textDelimiter = QString("\""),
QStringConverter::Encoding codec
= QStringConverter::Utf8);
};
} // namespace QtCSV
#endif // QTCSVREADER_H

View File

@ -0,0 +1,66 @@
#ifndef QTCSVSTRINGDATA_H
#define QTCSVSTRINGDATA_H
#include "qtcsv/abstractdata.h"
#include "qtcsv/qtcsv_global.h"
#include <QList>
#include <QString>
namespace QtCSV
{
// StringData is a simple container class. It implements interface of
// AbstractData class and uses strings to store information. Also it
// provides basic functions for working with rows.
class QTCSVSHARED_EXPORT StringData : public AbstractData
{
QList<QList<QString>> m_values;
public:
StringData() = default;
StringData(const StringData &other);
StringData &operator=(const StringData &other);
~StringData() override = default;
bool operator==(const StringData &other) const;
// Add new empty row
void addEmptyRow() override;
// Add new row with one value
void addRow(const QString &value);
// Add new row with specified values (as strings)
void addRow(const QList<QString> &values) override;
// Clear all data
void clear() override;
// Insert new row at index position 'row'
void insertRow(qsizetype row, const QString &value);
void insertRow(qsizetype row, const QList<QString> &values);
// Check if there are any data
bool isEmpty() const override;
// Remove the row at index position 'row'
void removeRow(qsizetype row);
// Replace the row at index position 'row' with new row
void replaceRow(qsizetype row, const QString &value);
void replaceRow(qsizetype row, const QList<QString> &values);
// Reserve space for 'size' rows
void reserve(qsizetype size);
// Get number of rows
qsizetype rowCount() const override;
// Get values (as list of strings) of specified row
QList<QString> rowValues(qsizetype row) const override;
// Add new row that would contain one value
StringData &operator<<(const QString &value);
// Add new row with specified values
StringData &operator<<(const QList<QString> &values);
};
inline bool operator!=(const StringData &left, const StringData &right)
{
return !(left == right);
}
} // namespace QtCSV
#endif // QTCSVSTRINGDATA_H

View File

@ -0,0 +1,76 @@
#ifndef QTCSVVARIANTDATA_H
#define QTCSVVARIANTDATA_H
#include "qtcsv/abstractdata.h"
#include "qtcsv/qtcsv_global.h"
#include <QList>
#include <QString>
#include <QVariant>
namespace QtCSV
{
// VariantData is a simple container class. It implements interface of
// AbstractData class. It uses QVariant to hold information, so data could
// be of almost any type - integral, strings and so on. There is only one
// limitation - QVariant must be convertible to string (because,
// obviously, if we want to save information to CSV file, we would need to
// convert it to plain-text form). So don't forget to see docs of QVariant
// before you start using this class.
class QTCSVSHARED_EXPORT VariantData : public AbstractData
{
QList<QList<QVariant>> m_values;
public:
VariantData() = default;
VariantData(const VariantData &other);
VariantData &operator=(const VariantData &other);
~VariantData() override = default;
bool operator==(const VariantData &other) const;
// Add new empty row
void addEmptyRow() override;
// Add new row with one value
bool addRow(const QVariant &value);
// Add new row with specified values
bool addRow(const QList<QVariant> &values);
// Add new row with specified values (as strings)
void addRow(const QList<QString> &values) override;
// Clear all data
void clear() override;
// Insert new row at index position 'row'
bool insertRow(qsizetype row, const QVariant &value);
bool insertRow(qsizetype row, const QList<QString> &values);
bool insertRow(qsizetype row, const QList<QVariant> &values);
// Check if there are any data
bool isEmpty() const override;
// Remove the row at index position 'row'
void removeRow(qsizetype row);
// Replace the row at index position 'row' with new row
bool replaceRow(qsizetype row, const QVariant &value);
bool replaceRow(qsizetype row, const QList<QString> &values);
bool replaceRow(qsizetype row, const QList<QVariant> &values);
// Reserve space for 'size' rows
void reserve(qsizetype size);
// Get number of rows
qsizetype rowCount() const override;
// Get values (as list of strings) of specified row
QList<QString> rowValues(qsizetype row) const override;
// Add new row that would contain one value
VariantData &operator<<(const QVariant &value);
// Add new row with specified values
VariantData &operator<<(const QList<QString> &values);
VariantData &operator<<(const QList<QVariant> &values);
};
inline bool operator!=(const VariantData &left, const VariantData &right)
{
return !(left == right);
}
} // namespace QtCSV
#endif // QTCSVVARIANTDATA_H

View File

@ -0,0 +1,54 @@
#ifndef QTCSVWRITER_H
#define QTCSVWRITER_H
#include "qtcsv/qtcsv_global.h"
#include "abstractdata.h"
#include <QIODevice>
#include <QList>
#include <QString>
#include <QStringConverter>
namespace QtCSV
{
class ContentIterator;
// Writer is a data-writer class that works with csv-files and IO Devices.
// As a source of information it requires AbstractData-based container
// class object.
//
// It supports different write methods:
// - WriteMode::REWRITE - if file exist, it will be rewritten
// - WriteMode::APPEND - if file exist, new information will be appended
// to the end of the file.
//
// Also you can specify header and footer for your data.
class QTCSVSHARED_EXPORT Writer
{
public:
enum class WriteMode
{
REWRITE = 0,
APPEND
};
// Write data to csv-file
static bool write(const QString &filePath, const AbstractData &data,
const QString &separator = QString(","),
const QString &textDelimiter = QString("\""),
WriteMode mode = WriteMode::REWRITE,
const QList<QString> &header = {},
const QList<QString> &footer = {},
QStringConverter::Encoding codec = QStringConverter::Utf8);
// Write data to IO Device
static bool write(QIODevice &ioDevice, const AbstractData &data,
const QString &separator = QString(","),
const QString &textDelimiter = QString("\""),
const QList<QString> &header = {},
const QList<QString> &footer = {},
QStringConverter::Encoding codec = QStringConverter::Utf8);
};
} // namespace QtCSV
#endif // QTCSVWRITER_H

24
lib/qtcsv/qtcsv.pri Normal file
View File

@ -0,0 +1,24 @@
CONFIG *= qt
QT *= core
!contains(DEFINES, QTCSV_LIBRARY): DEFINES += QTCSV_MAKE_LIB
INCLUDEPATH += $$PWD/include \
$$PWD
SOURCES += \
$$PWD/sources/writer.cpp \
$$PWD/sources/variantdata.cpp \
$$PWD/sources/stringdata.cpp \
$$PWD/sources/reader.cpp \
$$PWD/sources/contentiterator.cpp
HEADERS += \
$$PWD/include/qtcsv/qtcsv_global.h \
$$PWD/include/qtcsv/writer.h \
$$PWD/include/qtcsv/variantdata.h \
$$PWD/include/qtcsv/stringdata.h \
$$PWD/include/qtcsv/reader.h \
$$PWD/include/qtcsv/abstractdata.h \
$$PWD/sources/filechecker.h \
$$PWD/sources/contentiterator.h \
$$PWD/sources/symbols.h

50
lib/qtcsv/qtcsv.pro Normal file
View File

@ -0,0 +1,50 @@
QT = core
TARGET = qtcsv
TEMPLATE = lib
VERSION = 1.7.0
win32:TARGET_EXT = .dll
# Uncomment this setting if you want to build static library
#CONFIG += staticlib
!msvc {
# flags for gcc-like compiler
CONFIG += warn_on
QMAKE_CXXFLAGS_WARN_ON += -Werror -Wformat=2 -Wuninitialized -Winit-self \
-Wswitch-enum -Wundef -Wpointer-arith \
-Wdisabled-optimization -Wcast-align -Wcast-qual
}
CONFIG(staticlib): DEFINES += QTCSV_STATIC_LIB
DEFINES += QTCSV_LIBRARY
include(qtcsv.pri)
# Uncomment this settings if you want to manually set destination directory for
# compiled library
#CONFIG(release, debug|release): DESTDIR = $$PWD
#CONFIG(debug, debug|release): DESTDIR = $$PWD
DISTFILES += \
CMakeLists.txt
OTHER_FILES += \
appveyor.yml
message(=== Configuration of qtcsv ===)
message(Qt version: $$[QT_VERSION])
message(Library version: $$VERSION)
message(Library files will be created in folder: $$OUT_PWD)
unix {
# settings for command "make install"
copy_lib_headers.path = /usr/local/include/qtcsv/
copy_lib_headers.files = $$PWD/include/qtcsv/*.h
target.path = /usr/local/lib
INSTALLS += target copy_lib_headers
message(--- Settings for command \"make install\")
message(Library files will be copied to folder: $$target.path)
message(Library headers will be copied to folder: $$copy_lib_headers.path)
}

View File

@ -0,0 +1,135 @@
#include "sources/contentiterator.h"
#include "sources/symbols.h"
using namespace QtCSV;
// Constructor of ContentIterator
// @input:
// - data - AbstractData object
// - separator - string or character that would separate values in a row (line)
// - textDelimiter - string or character that enclose each element in a row
// - header - strings that will be placed on the first line
// - footer - strings that will be placed on the last line
// - chunkSize - size (in rows) of chunk of data
ContentIterator::ContentIterator(const AbstractData &data,
const QString &separator,
const QString &textDelimiter,
const QList<QString> &header,
const QList<QString> &footer,
const qsizetype chunkSize)
: m_data(data)
, m_separator(separator)
, m_textDelimiter(textDelimiter)
, m_header(header)
, m_footer(footer)
, m_chunkSize(chunkSize)
, m_dataRow(-1)
, m_atEnd(false)
{
}
// Check if content contains information
// @output:
// - bool - True if content is empty, False otherwise
bool ContentIterator::isEmpty() const
{
return m_data.isEmpty() && m_header.isEmpty() && m_footer.isEmpty();
}
// Check if content still has chunks of information to return
// @output:
// - bool - True if class can return next chunk of information, False otherwise
bool ContentIterator::hasNext() const
{
return !m_atEnd;
}
// Get next chunk of information
// @output:
// - QString - next chunk of information. If there is no more information to
// return, function will return empty string
QString ContentIterator::getNext()
{
// Check if we have already get to the end of the content
if (m_atEnd)
{
return {};
}
QString content;
qsizetype rowsNumber = 0;
// Initially m_dataRow have negative value. Negative value indicates that
// client have called this function first time. In this case at the
// beginning of the chunk we should place header information. And then
// set m_dataRow to the index of the first row in main data container.
if (m_dataRow < 0)
{
if (!m_header.isEmpty())
{
content.append(composeRow(m_header));
++rowsNumber;
}
m_dataRow = 0;
}
// Check if m_dataRow is less than number of rows in m_data. If this is
// true, add information from the m_data to the chunk. Otherwise, this means
// that we already have passed all information from the m_data.
if (m_dataRow < m_data.rowCount())
{
const auto endRow
= qMin(m_dataRow + m_chunkSize - rowsNumber, m_data.rowCount());
for (auto i = m_dataRow; i < endRow; ++i, ++m_dataRow, ++rowsNumber)
{
content.append(composeRow(m_data.rowValues(i)));
}
}
// If we still have place in chunk, try to add footer information to it.
if (rowsNumber < m_chunkSize)
{
if (!m_footer.isEmpty())
{
content.append(composeRow(m_footer));
++rowsNumber;
}
// At this point chunk contains the last row - footer. That
// means that we get to the end of content.
m_atEnd = true;
}
return content;
}
// Compose row string from values
// @input:
// - values - list of values in rows
// @output:
// - QString - result row string
QString ContentIterator::composeRow(const QList<QString> &values) const
{
QList<QString> rowValues = values;
const QString twoDelimiters = m_textDelimiter + m_textDelimiter;
for (auto i = 0; i < rowValues.size(); ++i)
{
rowValues[i].replace(m_textDelimiter, twoDelimiters);
QString delimiter = m_textDelimiter;
if (delimiter.isEmpty()
&& (rowValues.at(i).contains(m_separator)
|| rowValues.at(i).contains(CR) || rowValues.at(i).contains(LF)))
{
delimiter = DOUBLE_QUOTE;
}
rowValues[i].prepend(delimiter);
rowValues[i].append(delimiter);
}
QString result = rowValues.join(m_separator);
result.append(LF);
return result;
}

View File

@ -0,0 +1,48 @@
#ifndef QTCSVCONTENTITERATOR_H
#define QTCSVCONTENTITERATOR_H
#include "include/qtcsv/abstractdata.h"
#include <QList>
#include <QString>
namespace QtCSV
{
// ContentIterator is a class that holds references to containers with
// information. Its main purpose:
// - to separate information into a chunks and
// - to return these chunks one by one to the client.
// You can use this class as forward-iterator that can go (only once) from
// the beginning to the end of the data.
// You can use this class with csv-writer class. ContentIterator will join
// elements of one row with separator symbol and then join rows with
// new line symbol.
class ContentIterator
{
const AbstractData &m_data;
const QString &m_separator;
const QString &m_textDelimiter;
const QList<QString> &m_header;
const QList<QString> &m_footer;
const qsizetype m_chunkSize;
qsizetype m_dataRow;
bool m_atEnd;
// Compose row string from values
QString composeRow(const QList<QString> &values) const;
public:
ContentIterator(const AbstractData &data, const QString &separator,
const QString &textDelimiter, const QList<QString> &header,
const QList<QString> &footer, qsizetype chunkSize = 1000);
// Check if content contains information
bool isEmpty() const;
// Check if content still has chunks of information to return
bool hasNext() const;
// Get next chunk of information
QString getNext();
};
} // namespace QtCSV
#endif // QTCSVCONTENTITERATOR_H

View File

@ -0,0 +1,43 @@
#ifndef QTCSVFILECHECKER_H
#define QTCSVFILECHECKER_H
#include <QDebug>
#include <QFileInfo>
#include <QString>
namespace QtCSV
{
// Check if path to csv file is valid
// @input:
// - filePath - string with absolute path to file
// - mustExist - True if file must exist, False if this is not important
// @output:
// - bool - True if file is OK, else False
inline bool CheckFile(const QString &filePath, const bool mustExist = false)
{
if (filePath.isEmpty())
{
qDebug() << __FUNCTION__ << "Error - file path is empty";
return false;
}
QFileInfo fileInfo(filePath);
if (fileInfo.isAbsolute() && false == fileInfo.isDir())
{
if (mustExist && false == fileInfo.exists())
{
return false;
}
if ("csv" != fileInfo.suffix())
{
qDebug() << __FUNCTION__ << "Warning - file suffix is not .csv";
}
return true;
}
return false;
}
} // namespace QtCSV
#endif // QTCSVFILECHECKER_H

View File

@ -0,0 +1,617 @@
#include "include/qtcsv/reader.h"
#include "include/qtcsv/abstractdata.h"
#include "sources/filechecker.h"
#include "sources/symbols.h"
#include <QDebug>
#include <QFile>
#include <QStringView>
#include <QTextStream>
using namespace QtCSV;
bool openFile(const QString &filePath, QFile &file)
{
if (!CheckFile(filePath, true))
{
qDebug() << __FUNCTION__ << "Error - wrong file path:" << filePath;
return false;
}
file.setFileName(filePath);
const auto result = file.open(QIODevice::ReadOnly);
if (!result)
{
qDebug() << __FUNCTION__ << "Error - can't open file:" << filePath;
}
return result;
}
// ElementInfo is a helper struct that is used as indicator of row end
struct ElementInfo
{
bool isEnded = true;
};
class ReaderPrivate
{
// Check if file path and separator are valid
static bool checkParams(const QString &separator);
// Split string to elements
static QList<QString> splitElements(const QString &line,
const QString &separator,
const QString &textDelimiter,
ElementInfo &elemInfo);
// Try to find end position of first or middle element
static qsizetype findMiddleElementPosition(const QString &str,
const qsizetype &startPos,
const QString &separator,
const QString &txtDelim);
// Check if current element is the last element
static bool isElementLast(const QString &str, const qsizetype startPos,
const QString &separator, const QString &txtDelim);
// Remove extra symbols (spaces, text delimeters...)
static void removeExtraSymbols(QList<QString> &elements,
const QString &textDelimiter);
public:
// Function that really reads csv-data and transfer it's data to
// AbstractProcessor-based processor
static bool read(QIODevice &ioDevice, Reader::AbstractProcessor &processor,
const QString &separator, const QString &textDelimiter,
QStringConverter::Encoding codec);
};
// Function that really reads csv-data and transfer it's data to
// AbstractProcessor-based processor
// @input:
// - ioDevice - IO Device containing the csv-formatted data
// - processor - refernce to AbstractProcessor-based object
// - separator - string or character that separate values in a row
// - textDelimiter - string or character that enclose row elements
// - codec - pointer to codec object that would be used for file reading
// @output:
// - bool - result of read operation
bool ReaderPrivate::read(QIODevice &ioDevice,
Reader::AbstractProcessor &processor,
const QString &separator, const QString &textDelimiter,
const QStringConverter::Encoding codec)
{
if (!checkParams(separator))
{
return false;
}
// Open IO Device if it was not opened
if (!ioDevice.isOpen() && !ioDevice.open(QIODevice::ReadOnly))
{
qDebug() << __FUNCTION__ << "Error - failed to open IO Device";
return false;
}
QTextStream stream(&ioDevice);
stream.setEncoding(codec);
// This list will contain elements of the row if its elements
// are located on several lines
QList<QString> row;
ElementInfo elemInfo;
auto result = true;
while (!stream.atEnd())
{
auto line = stream.readLine();
processor.preProcessRawLine(line);
auto elements = ReaderPrivate::splitElements(line, separator, textDelimiter,
elemInfo);
if (elemInfo.isEnded)
{
// Current row ends on this line. Check if these elements are
// end elements of the long row
if (row.isEmpty())
{
// No, these elements constitute the entire row
if (!processor.processRowElements(elements))
{
result = false;
break;
}
}
else
{
// Yes, these elements should be added to the row
if (!elements.isEmpty())
{
row.last().append(elements.takeFirst());
row << elements;
}
if (!processor.processRowElements(row))
{
result = false;
break;
}
row.clear();
}
}
else
{
// These elements constitute long row that lasts on several lines
if (!elements.isEmpty())
{
if (!row.isEmpty())
{
row.last().append(elements.takeFirst());
}
row << elements;
}
}
}
if (!elemInfo.isEnded && !row.isEmpty())
{
result = processor.processRowElements(row);
}
return result;
}
// Check if file path and separator are valid
// @input:
// - separator - string or character that separate values in a row
// @output:
// - bool - True if file path and separator are valid, otherwise False
bool ReaderPrivate::checkParams(const QString &separator)
{
if (separator.isEmpty())
{
qDebug() << __FUNCTION__ << "Error - separator could not be empty";
return false;
}
return true;
}
// Split string to elements
// @input:
// - line - string with data
// - separator - string or character that separate elements
// - textDelimiter - string that is used as text delimiter
// @output:
// - QList<QString> - list of elements
QList<QString> ReaderPrivate::splitElements(const QString &line,
const QString &separator,
const QString &textDelimiter,
ElementInfo &elemInfo)
{
// If separator is empty, return whole line. Can't work in this
// conditions!
if (separator.isEmpty())
{
elemInfo.isEnded = true;
return (QList<QString>() << line);
}
if (line.isEmpty())
{
// If previous row was ended, then return empty QList<QString>.
// Otherwise return list that contains one element - new line symbols
return elemInfo.isEnded ? QList<QString>() : (QList<QString>() << LF);
}
QList<QString> result;
qsizetype pos = 0;
while (pos < line.size())
{
if (elemInfo.isEnded)
{
// This line is a new line, not a continuation of the previous
// line.
// Check if element starts with the delimiter symbol
const auto delimiterPos = line.indexOf(textDelimiter, pos);
if (delimiterPos == pos)
{
pos = delimiterPos + textDelimiter.size();
// Element starts with the delimiter symbol. It means that
// this element could contain any number of double
// delimiters and separator symbols. This element could:
// 1. Be the first or the middle element. Then it should end
// with delimiter and the seprator symbols standing next to each
// other.
const auto midElemEndPos
= findMiddleElementPosition(line, pos, separator, textDelimiter);
if (midElemEndPos > 0)
{
const auto length = midElemEndPos - pos;
result << line.mid(pos, length);
pos = midElemEndPos + textDelimiter.size() + separator.size();
continue;
}
// 2. Be The last element on the line. Then it should end with
// delimiter symbol.
if (isElementLast(line, pos, separator, textDelimiter))
{
const auto length = line.size() - textDelimiter.size() - pos;
result << line.mid(pos, length);
break;
}
// 3. Not ends on this line
const auto length = line.size() - pos;
result << line.mid(pos, length);
elemInfo.isEnded = false;
break;
}
else
{
// Element do not starts with the delimiter symbol. It means
// that this element do not contain double delimiters and it
// ends at the next separator symbol.
// Check if line contains separator symbol.
const auto separatorPos = line.indexOf(separator, pos);
if (separatorPos >= 0)
{
// If line contains separator symbol, then our element
// located between current position and separator
// position. Copy it into result list and move
// current position over the separator position.
result << line.mid(pos, separatorPos - pos);
// Special case: if line ends with separator symbol,
// then at the end of the line we have empty element.
if (separatorPos == line.size() - separator.size())
{
result << QString();
}
// Move the current position on to the next element
pos = separatorPos + separator.size();
}
else
{
// If line do not contains separator symbol, then
// this element ends at the end of the string.
// Copy it into result list and exit the loop.
result << line.mid(pos);
break;
}
}
}
else
{
// This line is a continuation of the previous. Last element of the
// previous line did not end. It started with delimiter symbol.
// It means that this element could contain any number of double
// delimiters and separator symbols. This element could:
// 1. Ends somewhere in the middle of the line. Then it should ends
// with delimiter and the seprator symbols standing next to each
// other.
const auto midElemEndPos
= findMiddleElementPosition(line, pos, separator, textDelimiter);
if (midElemEndPos >= 0)
{
result << (LF + line.mid(pos, midElemEndPos - pos));
pos = midElemEndPos + textDelimiter.size() + separator.size();
elemInfo.isEnded = true;
continue;
}
// 2. Ends at the end of the line. Then it should ends with
// delimiter symbol.
if (isElementLast(line, pos, separator, textDelimiter))
{
const auto length = line.size() - textDelimiter.size() - pos;
result << (LF + line.mid(pos, length));
elemInfo.isEnded = true;
break;
}
// 3. Not ends on this line
result << (LF + line);
break;
}
}
removeExtraSymbols(result, textDelimiter);
return result;
}
// Try to find end position of first or middle element
// @input:
// - str - string with data
// - startPos - start position of the current element in the string
// - separator - string or character that separate elements
// - textDelimiter - string that is used as text delimiter
// @output:
// - qsizetype - end position of the element or -1 if this element is not first
// or middle
qsizetype ReaderPrivate::findMiddleElementPosition(const QString &str,
const qsizetype &startPos,
const QString &separator,
const QString &txtDelim)
{
const qsizetype ERROR = -1;
if (str.isEmpty() || startPos < 0 || separator.isEmpty()
|| txtDelim.isEmpty())
{
return ERROR;
}
const auto elemEndSymbols = txtDelim + separator;
auto elemEndPos = startPos;
while (elemEndPos < str.size())
{
// Find position of element end symbol
elemEndPos = str.indexOf(elemEndSymbols, elemEndPos);
if (elemEndPos < 0)
{
// This element could not be the middle element, becaise string
// do not contains any end symbols
return ERROR;
}
// Check that this is really the end symbols of the
// element and we don't mix up it with double delimiter
// and separator. Calc number of delimiter symbols from elemEndPos
// to startPos that stands together.
qsizetype numOfDelimiters = 0;
for (auto pos = elemEndPos; startPos <= pos; --pos, ++numOfDelimiters)
{
const auto strRef = str.mid(pos, txtDelim.size());
if (QString::compare(strRef, txtDelim) != 0)
{
break;
}
}
// If we have odd number of delimiter symbols that stand together,
// then this is the even number of double delimiter symbols + last
// delimiter symbol. That means that we have found end position of
// the middle element.
if (numOfDelimiters % 2 == 1)
{
return elemEndPos;
}
else
{
// Otherwise this is not the end of the middle element and we
// should try again
elemEndPos += elemEndSymbols.size();
}
}
return ERROR;
}
// Check if current element is the last element
// @input:
// - str - string with data
// - startPos - start position of the current element in the string
// - separator - string or character that separate elements
// - textDelimiter - string that is used as text delimiter
// @output:
// - bool - True if the current element is the last element of the string,
// False otherwise
bool ReaderPrivate::isElementLast(const QString &str, const qsizetype startPos,
const QString &separator,
const QString &txtDelim)
{
if (str.isEmpty() || startPos < 0 || separator.isEmpty()
|| txtDelim.isEmpty())
{
return false;
}
// Check if string ends with text delimiter. If not, then this element
// do not ends on this line
if (!str.endsWith(txtDelim))
{
return false;
}
// Check that this is really the end symbols of the
// element and we don't mix up it with double delimiter.
// Calc number of delimiter symbols from end
// to startPos that stands together.
qsizetype numOfDelimiters = 0;
for (auto pos = str.size() - 1; startPos <= pos; --pos, ++numOfDelimiters)
{
const auto strRef = str.mid(pos, txtDelim.size());
if (QString::compare(strRef, txtDelim) != 0)
{
break;
}
}
// If we have odd number of delimiter symbols that stand together,
// then this is the even number of double delimiter symbols + last
// delimiter symbol. That means that this element is the last on the line.
return numOfDelimiters % 2 == 1;
}
// Remove extra symbols (spaces, text delimeters...)
// @input:
// - elements - list of row elements
// - textDelimiter - string that is used as text delimiter
void ReaderPrivate::removeExtraSymbols(QList<QString> &elements,
const QString &textDelimiter)
{
if (elements.isEmpty())
{
return;
}
const auto doubleTextDelim = textDelimiter + textDelimiter;
for (auto i = 0; i < elements.size(); ++i)
{
const auto str = QStringView{elements.at(i)};
if (str.isEmpty())
{
continue;
}
qsizetype startPos = 0, endPos = str.size() - 1;
// Find first non-space char
for (; startPos < str.size()
&& str.at(startPos).category() == QChar::Separator_Space;
++startPos)
;
// Find last non-space char
for (; endPos >= 0 && str.at(endPos).category() == QChar::Separator_Space;
--endPos)
;
if (!textDelimiter.isEmpty())
{
// Skip text delimiter symbol if element starts with it
const auto strStart = str.mid(startPos, textDelimiter.size());
if (strStart == textDelimiter)
{
startPos += textDelimiter.size();
}
// Skip text delimiter symbol if element ends with it
const auto strEnd
= str.mid(endPos - textDelimiter.size() + 1, textDelimiter.size());
if (strEnd == textDelimiter)
{
endPos -= textDelimiter.size();
}
}
if ((0 < startPos || endPos < str.size() - 1) && startPos <= endPos)
{
elements[i] = elements[i].mid(startPos, endPos - startPos + 1);
}
// Also replace double text delimiter with one text delimiter symbol
elements[i].replace(doubleTextDelim, textDelimiter);
}
}
// ReadToListProcessor - processor that saves rows of elements to list.
class ReadToListProcessor : public Reader::AbstractProcessor
{
public:
QList<QList<QString>> data;
bool processRowElements(const QList<QString> &elements) override
{
data << elements;
return true;
}
};
// Read csv-file and save it's data as strings to QList<QList<QString>>
// @input:
// - filePath - string with absolute path to csv-file
// - separator - string or character that separate elements in a row
// - textDelimiter - string or character that enclose each element in a row
// - codec - pointer to codec object that would be used for file reading
// @output:
// - QList<QList<QString>> - list of values (as strings) from csv-file. In case
// of error will return empty QList<QList<QString>>.
QList<QList<QString>> Reader::readToList(const QString &filePath,
const QString &separator,
const QString &textDelimiter,
const QStringConverter::Encoding codec)
{
QFile file;
return openFile(filePath, file)
? readToList(file, separator, textDelimiter, codec)
: QList<QList<QString>>();
}
// Read csv-formatted data from IO Device and save it
// as strings to QList<QList<QString>>
QList<QList<QString>> Reader::readToList(QIODevice &ioDevice,
const QString &separator,
const QString &textDelimiter,
const QStringConverter::Encoding codec)
{
ReadToListProcessor processor;
ReaderPrivate::read(ioDevice, processor, separator, textDelimiter, codec);
return processor.data;
}
// Read csv-file and save it's data to AbstractData-based container class
// @input:
// - filePath - string with absolute path to csv-file
// - data - AbstractData object where all file content will be saved
// - separator - string or character that separate elements in a row
// - textDelimiter - string or character that enclose each element in a row
// - codec - pointer to codec object that would be used for file reading
// @output:
// - bool - True if file was successfully read, otherwise False
bool Reader::readToData(const QString &filePath, AbstractData &data,
const QString &separator, const QString &textDelimiter,
const QStringConverter::Encoding codec)
{
QFile file;
return openFile(filePath, file)
? readToData(file, data, separator, textDelimiter, codec)
: false;
}
// Read csv-formatted data from IO Device and save it
// to AbstractData-based container class
bool Reader::readToData(QIODevice &ioDevice, AbstractData &data,
const QString &separator, const QString &textDelimiter,
const QStringConverter::Encoding codec)
{
ReadToListProcessor processor;
const auto result = ReaderPrivate::read(ioDevice, processor, separator,
textDelimiter, codec);
if (result)
{
for (auto i = 0; i < processor.data.size(); ++i)
{
data.addRow(processor.data.at(i));
}
}
return result;
}
// Read csv-file and process it line-by-line
// @input:
// - filePath - string with absolute path to csv-file
// - processor - AbstractProcessor-based object that receives data from
// csv-file line-by-line
// - separator - string or character that separate elements in a row
// - textDelimiter - string or character that enclose each element in a row
// - codec - pointer to codec object that would be used for file reading
// @output:
// - bool - True if file was successfully read, otherwise False
bool Reader::readToProcessor(const QString &filePath,
Reader::AbstractProcessor &processor,
const QString &separator,
const QString &textDelimiter,
const QStringConverter::Encoding codec)
{
QFile file;
return openFile(filePath, file)
? readToProcessor(file, processor, separator, textDelimiter, codec)
: false;
}
// Read csv-formatted data from IO Device and process it line-by-line
bool Reader::readToProcessor(QIODevice &ioDevice,
Reader::AbstractProcessor &processor,
const QString &separator,
const QString &textDelimiter,
const QStringConverter::Encoding codec)
{
return ReaderPrivate::read(ioDevice, processor, separator, textDelimiter,
codec);
}

View File

@ -0,0 +1,156 @@
#include "include/qtcsv/stringdata.h"
using namespace QtCSV;
StringData::StringData(const StringData &other)
: m_values(other.m_values)
{
}
StringData &StringData::operator=(const StringData &other)
{
m_values = other.m_values;
return *this;
}
bool StringData::operator==(const StringData &other) const
{
return m_values == other.m_values;
}
// Add new empty row
void StringData::addEmptyRow()
{
m_values << QList<QString>();
}
// Add new row with one value
// @input:
// - value - value that is supposed to be written to the new row
void StringData::addRow(const QString &value)
{
m_values << (QList<QString>() << value);
}
// Add new row with specified values (as strings)
// @input:
// - values - list of strings. If list is empty, it will be interpreted
// as empty line
void StringData::addRow(const QList<QString> &values)
{
m_values << values;
}
// Clear all data
void StringData::clear()
{
m_values.clear();
}
// Insert new row at index position 'row'.
// @input:
// - row - index of row. If 'row' is 0, the value will be set as first row.
// If 'row' is >= rowCount(), the value will be added as new last row.
// - value - value that is supposed to be written to the new row
void StringData::insertRow(const qsizetype row, const QString &value)
{
insertRow(row, (QList<QString>() << value));
}
// Insert new row at index position 'row'.
// @input:
// - row - index of row. If 'row' is 0, the values will be set as first row.
// If 'row' is >= rowCount(), the values will be added as new last row.
// - values - list of strings
void StringData::insertRow(const qsizetype row, const QList<QString> &values)
{
m_values.insert(qBound(0, row, m_values.size()), values);
}
// Check if there are any rows
// @output:
// - bool - True if there are any rows, else False
bool StringData::isEmpty() const
{
return m_values.isEmpty();
}
// Remove the row at index position 'row'.
// @input:
// - row - index of row to remove. 'row' must be a valid index position
// (i.e., 0 <= row < rowCount()). Otherwise function will do nothing.
void StringData::removeRow(const qsizetype row)
{
if (row >= 0 && row < m_values.size())
{
m_values.removeAt(row);
}
}
// Replace the row at index position 'row' with new row.
// @input:
// - row - index of row that should be replaced. 'row' must be
// a valid index position (i.e., 0 <= row < rowCount()).
// - value - value that is supposed to be written instead of the 'old' values
void StringData::replaceRow(const qsizetype row, const QString &value)
{
replaceRow(row, (QList<QString>() << value));
}
// Replace the row at index position 'row' with new row.
// @input:
// - row - index of row that should be replaced. 'row' must be
// a valid index position (i.e., 0 <= row < rowCount()).
// - values - list of strings that is supposed to be written instead of the
// 'old' values
void StringData::replaceRow(const qsizetype row, const QList<QString> &values)
{
m_values.replace(row, values);
}
// Reserve space for 'size' rows.
// @input:
// - size - number of rows to reserve in memory. If 'size' is smaller than the
// current number of rows, function will do nothing.
void StringData::reserve(const qsizetype size)
{
m_values.reserve(size);
}
// Get number of rows
// @output:
// - qsizetype - current number of rows
qsizetype StringData::rowCount() const
{
return m_values.size();
}
// Get values (as list of strings) of specified row
// @input:
// - row - valid number of row
// @output:
// - QList<QString> - values of row. If row is invalid number, function will
// return empty list.
QList<QString> StringData::rowValues(const qsizetype row) const
{
if (row < 0 || rowCount() <= row)
{
return {};
}
return m_values.at(row);
}
// Add new row that would contain one value
StringData &StringData::operator<<(const QString &value)
{
addRow(value);
return *this;
}
// Add new row with specified values
StringData &StringData::operator<<(const QList<QString> &values)
{
addRow(values);
return *this;
}

View File

@ -0,0 +1,13 @@
#ifndef SYMBOLS_H
#define SYMBOLS_H
#include <QString>
namespace QtCSV
{
const QString DOUBLE_QUOTE("\"");
const QString CR("\r");
const QString LF("\n");
} // namespace QtCSV
#endif // SYMBOLS_H

View File

@ -0,0 +1,277 @@
#include "include/qtcsv/variantdata.h"
using namespace QtCSV;
// Check if all values are convertable to strings
// @input:
// - values - list of values
// @output:
// - bool - True if all values are convertable to strings, False otherwise
bool isConvertableToString(const QList<QVariant> &values)
{
for (auto iter = values.constBegin(); iter != values.constEnd(); ++iter)
{
if (!(*iter).canConvert<QString>())
{
return false;
}
}
return true;
}
// Transform QList<QString> to QList<QVariant>
// @input:
// - values - list of strings
// @output:
// - QList<QVariant> - list of the same strings, but converted to QVariants
QList<QVariant> toListOfVariants(const QList<QString> &values)
{
QList<QVariant> list;
for (auto iter = values.constBegin(); iter != values.constEnd(); ++iter)
{
list << QVariant(*iter);
}
return list;
}
VariantData::VariantData(const VariantData &other)
: m_values(other.m_values)
{
}
VariantData &VariantData::operator=(const VariantData &other)
{
m_values = other.m_values;
return *this;
}
bool VariantData::operator==(const VariantData &other) const
{
return m_values == other.m_values;
}
// Add new empty row
void VariantData::addEmptyRow()
{
m_values << QList<QVariant>();
}
// Add new row with one value
// @input:
// - value - value that is supposed to be written to the new row.
// If value is empty, empty row will be added. Value must be convertable to a
// QString!
// @output:
// - bool - True if new row was successfully added, else False
bool VariantData::addRow(const QVariant &value)
{
if (!value.canConvert<QString>())
{
return false;
}
m_values << (QList<QVariant>() << value);
return true;
}
// Add new row with list of values
// @input:
// - values - list of values. If list is empty, empty row will be added.
// Values must be convertable to a QString!
// @output:
// - bool - True if new row was successfully added, else False
bool VariantData::addRow(const QList<QVariant> &values)
{
if (!isConvertableToString(values))
{
return false;
}
m_values << values;
return true;
}
// Add new row with specified values (as strings)
// @input:
// - values - list of strings. If list is empty, empty row will be added.
void VariantData::addRow(const QList<QString> &values)
{
m_values << toListOfVariants(values);
}
// Clear all data
void VariantData::clear()
{
m_values.clear();
}
// Insert new row at index position 'row'.
// @input:
// - row - index of row. If 'row' is 0, the value will be set as first row.
// If 'row' is >= rowCount(), the value will be added as new last row.
// - value - value that is supposed to be written to the new row. Value must be
// convertable to a QString!
// @output:
// - bool - True if row was inserted, False otherwise
bool VariantData::insertRow(const qsizetype row, const QVariant &value)
{
return insertRow(row, (QList<QVariant>() << value));
}
// Insert new row at index position 'row'.
// @input:
// - row - index of row. If 'row' is 0, the value will be set as first row.
// If 'row' is >= rowCount(), the values will be added as new last row.
// - values - list of strings that are supposed to be written to the new row
// @output:
// - bool - True if row was inserted, False otherwise
bool VariantData::insertRow(const qsizetype row, const QList<QString> &values)
{
return insertRow(row, toListOfVariants(values));
}
// Insert new row at index position 'row'.
// @input:
// - row - index of row. If 'row' is 0, the value will be set as first row.
// If 'row' is >= rowCount(), the values will be added as new last row.
// - values - list of values that are supposed to be written to the new row.
// Values must be convertable to a QString!
// @output:
// - bool - True if row was inserted, False otherwise
bool VariantData::insertRow(const qsizetype row, const QList<QVariant> &values)
{
if (!isConvertableToString(values))
{
return false;
}
m_values.insert(qBound(0, row, m_values.size()), values);
return true;
}
// Check if there are any rows
// @output:
// - bool - True if there are any rows, else False
bool VariantData::isEmpty() const
{
return m_values.isEmpty();
}
// Remove the row at index position 'row'
// @input:
// - row - index of row to remove. 'row' must be a valid index position
// (i.e., 0 <= row < rowCount()). Otherwise function will do nothing.
void VariantData::removeRow(const qsizetype row)
{
if (row >= 0 && row < m_values.size())
{
m_values.removeAt(row);
}
}
// Replace the row at index position 'row' with new row.
// @input:
// - row - index of row that should be replaced. 'row' must be
// a valid index position (i.e., 0 <= row < rowCount()).
// - value - value that is supposed to be written instead of the 'old' values.
// Value must be convertable to QString!
// @output:
// - bool - True if row was replaced, else False
bool VariantData::replaceRow(const qsizetype row, const QVariant &value)
{
return replaceRow(row, (QList<QVariant>() << value));
}
// Replace the row at index position 'row' with new row.
// @input:
// - row - index of row that should be replaced. 'row' must be
// a valid index position (i.e., 0 <= row < rowCount()).
// - values - values that are supposed to be written instead of the 'old'
// values.
// @output:
// - bool - True if row was replaced, else False
bool VariantData::replaceRow(const qsizetype row, const QList<QString> &values)
{
return replaceRow(row, toListOfVariants(values));
}
// Replace the row at index position 'row' with new row.
// @input:
// - row - index of row that should be replaced. 'row' must be
// a valid index position (i.e., 0 <= row < rowCount()).
// - values - values that are supposed to be written instead of the 'old'
// values. Values must be convertable to a QString!
// @output:
// - bool - True if row was replaced, else False
bool VariantData::replaceRow(const qsizetype row, const QList<QVariant> &values)
{
if (!isConvertableToString(values))
{
return false;
}
m_values.replace(row, values);
return true;
}
// Reserve space for 'size' rows.
// @input:
// - size - number of rows to reserve in memory. If 'size' is smaller than the
// current number of rows, function will do nothing.
void VariantData::reserve(const qsizetype size)
{
m_values.reserve(size);
}
// Get number of rows
// @output:
// - qsizetype - current number of rows
qsizetype VariantData::rowCount() const
{
return m_values.size();
}
// Get values (as list of strings) of specified row
// @input:
// - row - valid number of the row
// @output:
// - QList<QString> - values of the row. If row have invalid value, function
// will return empty QList<QString>.
QList<QString> VariantData::rowValues(const qsizetype row) const
{
if (row < 0 || rowCount() <= row)
{
return {};
}
QList<QString> values;
for (int i = 0; i < m_values.at(row).size(); ++i)
{
values << m_values.at(row).at(i).toString();
}
return values;
}
// Add new row that would contain one value
VariantData &VariantData::operator<<(const QVariant &value)
{
addRow(value);
return *this;
}
// Add new row with specified values
VariantData &VariantData::operator<<(const QList<QVariant> &values)
{
addRow(values);
return *this;
}
// Add new row with specified values
VariantData &VariantData::operator<<(const QList<QString> &values)
{
addRow(values);
return *this;
}

View File

@ -0,0 +1,269 @@
#include "include/qtcsv/writer.h"
#include "sources/contentiterator.h"
#include "sources/filechecker.h"
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QRandomGenerator>
#include <QTextStream>
#include <limits>
using namespace QtCSV;
// Class TempFileHandler is a helper class. Its main purpose is to delete file
// on destruction. It is like a "smart pointer" but for temporary file. When you
// create object of class TempFileHandler, you must specify absolute path
// to the (temp) file (as a string). When object will be about to destroy, it
// will try to remove specified file.
class TempFileHandler
{
QString m_filePath;
public:
explicit TempFileHandler(const QString &filePath)
: m_filePath(filePath)
{
}
~TempFileHandler() { QFile::remove(m_filePath); }
};
class WriterPrivate
{
public:
// Append information to the file
static bool appendToFile(const QString &filePath, ContentIterator &content,
QStringConverter::Encoding codec);
// Overwrite file with new information
static bool overwriteFile(const QString &filePath, ContentIterator &content,
QStringConverter::Encoding codec);
// Write to IO Device
static bool writeToIODevice(QIODevice &ioDevice, ContentIterator &content,
QStringConverter::Encoding codec);
// Create unique name for the temporary file
static QString getTempFileName();
};
// Append information to the file
// @input:
// - filePath - string with absolute path to csv-file
// - content - not empty handler of content for csv-file
// - codec - pointer to codec object that would be used for file writing
// @output:
// - bool - True if data was appended to the file, otherwise False
bool WriterPrivate::appendToFile(const QString &filePath,
ContentIterator &content,
const QStringConverter::Encoding codec)
{
if (filePath.isEmpty() || content.isEmpty())
{
qDebug() << __FUNCTION__ << "Error - invalid arguments";
return false;
}
QFile csvFile(filePath);
if (!csvFile.open(QIODevice::Append | QIODevice::Text))
{
qDebug() << __FUNCTION__
<< "Error - can't open file:" << csvFile.fileName();
return false;
}
const auto result = writeToIODevice(csvFile, content, codec);
csvFile.close();
return result;
}
// Overwrite file with new information
// @input:
// - filePath - string with absolute path to csv-file
// - content - not empty handler of content for csv-file
// - codec - pointer to codec object that would be used for file writing
// @output:
// - bool - True if file was overwritten with new data, otherwise False
bool WriterPrivate::overwriteFile(const QString &filePath,
ContentIterator &content,
const QStringConverter::Encoding codec)
{
// Create path to the unique temporary file
const auto tempFileName = getTempFileName();
if (tempFileName.isEmpty())
{
qDebug() << __FUNCTION__
<< "Error - failed to create unique name for temp file";
return false;
}
TempFileHandler handler(tempFileName);
// Write information to the temporary file
if (!appendToFile(tempFileName, content, codec))
{
return false;
}
// Remove "old" file if it exists
if (QFile::exists(filePath) && !QFile::remove(filePath))
{
qDebug() << __FUNCTION__ << "Error - failed to remove file" << filePath;
return false;
}
// Copy "new" file (temporary file) to the destination path (replace
// "old" file)
if (!QFile::copy(tempFileName, filePath))
{
qDebug() << __FUNCTION__ << "Error - failed to copy temp file to"
<< filePath;
return false;
}
return true;
}
// Write csv data to IO Device
// @input:
// - iodevice - IO Device to write data to
// - content - not empty handler of content for csv-file
// - codec - pointer to codec object that would be used for file writing
// @output:
// - bool - True if data could be written to the IO Device
bool WriterPrivate::writeToIODevice(QIODevice &ioDevice,
ContentIterator &content,
const QStringConverter::Encoding codec)
{
if (content.isEmpty())
{
qDebug() << __FUNCTION__ << "Error - invalid arguments";
return false;
}
// Open IO Device if it was not opened
if (!ioDevice.isOpen() && !ioDevice.open(QIODevice::Append | QIODevice::Text))
{
qDebug() << __FUNCTION__ << "Error - failed to open IO Device";
return false;
}
QTextStream stream(&ioDevice);
stream.setEncoding(codec);
while (content.hasNext())
{
stream << content.getNext();
}
stream.flush();
return stream.status() == QTextStream::Ok;
}
// Create unique name for the temporary file
// @output:
// - QString - string with the absolute path to the temporary file that is not
// exist yet. If function failed to create unique path, it will return empty
// string.
QString WriterPrivate::getTempFileName()
{
const auto nameTemplate
= QDir::tempPath() + "/qtcsv_"
+ QString::number(QCoreApplication::applicationPid()) + "_%1.csv";
for (auto counter = 0; counter < std::numeric_limits<int>::max(); ++counter)
{
QString name = nameTemplate.arg(
QString::number(QRandomGenerator::global()->generate()));
if (!QFile::exists(name))
{
return name;
}
}
return QString();
}
// Write data to csv-file
// @input:
// - filePath - string with absolute path to csv-file
// - data - not empty AbstractData object that contains information that should
// be written to csv-file
// - separator - string or character that would separate values in a row
// (line) in csv-file
// - textDelimiter - string or character that enclose each element in a row
// - mode - write mode of the file
// - header - strings that will be written at the beginning of the file in
// one line. separator will be used as delimiter character.
// - footer - strings that will be written at the end of the file in
// one line. separator will be used as delimiter character.
// - codec - pointer to codec object that would be used for file writing
// @output:
// - bool - True if data was written to the file, otherwise False
bool Writer::write(const QString &filePath, const AbstractData &data,
const QString &separator, const QString &textDelimiter,
const WriteMode mode, const QList<QString> &header,
const QList<QString> &footer,
const QStringConverter::Encoding codec)
{
if (filePath.isEmpty())
{
qDebug() << __FUNCTION__ << "Error - empty path to file";
return false;
}
if (data.isEmpty())
{
qDebug() << __FUNCTION__ << "Error - empty data";
return false;
}
if (false == CheckFile(filePath))
{
qDebug() << __FUNCTION__ << "Error - wrong file path/name:" << filePath;
return false;
}
ContentIterator content(data, separator, textDelimiter, header, footer);
switch (mode)
{
case WriteMode::APPEND:
return WriterPrivate::appendToFile(filePath, content, codec);
break;
case WriteMode::REWRITE:
default:
return WriterPrivate::overwriteFile(filePath, content, codec);
}
return false;
}
// Write data to IO Device
// @input:
// - ioDevice - IO Device
// - data - not empty AbstractData object that contains information that should
// be written to IO Device
// - separator - string or character that would separate values in a row
// - textDelimiter - string or character that enclose each element in a row
// - header - strings that will be written at the beginning of the csv-data in
// one line. separator will be used as delimiter character.
// - footer - strings that will be written at the end of the csv-data in
// one line. separator will be used as delimiter character.
// - codec - pointer to codec object that would be used for data writing
// @output:
// - bool - True if data was written to the IO Device, otherwise False
bool Writer::write(QIODevice &ioDevice, const AbstractData &data,
const QString &separator, const QString &textDelimiter,
const QList<QString> &header, const QList<QString> &footer,
const QStringConverter::Encoding codec)
{
if (data.isEmpty())
{
qDebug() << __FUNCTION__ << "Error - empty data";
return false;
}
ContentIterator content(data, separator, textDelimiter, header, footer);
return WriterPrivate::writeToIODevice(ioDevice, content, codec);
}

View File

@ -0,0 +1,24 @@
find_package(Qt6 COMPONENTS Test REQUIRED)
set(QT_TEST_TARGET Qt6::Test)
# define names
set(BINARY_NAME qtcsv_tests)
# instruct CMake to run moc automatically when needed.
set(CMAKE_AUTOMOC ON)
# add also the header part to source files. this is necessary for correct automoc
file(GLOB_RECURSE SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
add_executable(${BINARY_NAME} ${SOURCE_FILES} )
TARGET_LINK_LIBRARIES(${BINARY_NAME} PRIVATE ${QT_TEST_TARGET} ${PROJECT_NAME})
# provide current project dir for projects header search path
target_include_directories(${BINARY_NAME} PRIVATE .)
# copy test files after build
add_custom_command(TARGET ${BINARY_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/data ${CMAKE_CURRENT_BINARY_DIR}/data)

View File

@ -0,0 +1,5 @@
one,two,three
one_element
1,2,3
3.14
1 one,two,three
2 one_element
3 1,2,3
4 3.14

View File

@ -0,0 +1,7 @@
Year,Make,Model,Description,Price
1997,Ford,E350,"ac, abs, moon",3000.00
1999,Chevy,"Venture ""Extended Edition""","",4900.00
1996,Jeep,Grand Cherokee,"MUST SELL!
air, moon roof, loaded",4799.00
1999,Chevy,"Venture ""Extended Edition, Very Large""",,5000.00
,,"Venture ""Extended Edition""","",4900.00
1 Year Make Model Description Price
2 1997 Ford E350 ac, abs, moon 3000.00
3 1999 Chevy Venture "Extended Edition" 4900.00
4 1996 Jeep Grand Cherokee MUST SELL! air, moon roof, loaded 4799.00
5 1999 Chevy Venture "Extended Edition, Very Large" 5000.00
6 Venture "Extended Edition" 4900.00

View File

@ -0,0 +1,3 @@
one,two,"three, four",five
"this, is, one, element"
"six","seven","eight"
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,3 @@
one,two,'three, four',five
'this, is, one, element'
'six','seven','eight'
1 one,two,'three, four',five
2 'this, is, one, element'
3 'six','seven','eight'

View File

@ -0,0 +1,2 @@
"0";-;1;-;;-;
;-;"Question";-;158;-;;-;
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1 @@
"",0,,
1 0

View File

@ -0,0 +1,2 @@
"CCLK=""yy/MM/dd,hh:mm:ssA+zz""","test"
"new","line ""it is"",""def"
1 CCLK="yy/MM/dd,hh:mm:ssA+zz" test
2 new line "it is","def

View File

@ -0,0 +1,2 @@
Column1, Column2; Column3
" Hello with Hello again " ; "Hello Col 3"
Can't render this file because it contains an unexpected character in line 2 and column 28.

View File

@ -0,0 +1,5 @@
one,two,"three
four,"five
""six"",seven
eight",nine
ten,eleven
Can't render this file because it contains an unexpected character in line 2 and column 6.

View File

@ -0,0 +1,3 @@
one,two,"three
four",five
six,seven
Can't render this file because it has a wrong number of fields in line 3.

View File

@ -0,0 +1,3 @@
one,two,three
four,"very ""long"" field", five
six, seven
Can't render this file because it has a wrong number of fields in line 3.

View File

@ -0,0 +1,5 @@
A,B,C,D
a,b,c,d
a,"b field","c field
","d field"
aa,bb,cc,dd
1 A B C D
2 a b c d
3 a b field c field d field
4 aa bb cc dd

View File

@ -0,0 +1,2 @@
1;2;3
j;io;pp
1 1 2 3
2 j io pp

View File

@ -0,0 +1,2 @@
one,two,three
one_element
1 one,two,three
2 one_element

View File

@ -0,0 +1,561 @@
#include "testreader.h"
#include "qtcsv/reader.h"
#include "qtcsv/stringdata.h"
#include "qtcsv/variantdata.h"
#include <QDir>
#include <QFile>
#include <QElapsedTimer>
void TestReader::testReadToListInvalidArgs()
{
QVERIFY2(QtCSV::Reader::readToList(QString(), QString()).empty(),
"Invalid arguments was accepted");
QVERIFY2(
QtCSV::Reader::readToList(getPathToFileTestComma(), QString()).empty(),
"Invalid arguments was accepted");
QVERIFY2(QtCSV::Reader::readToList(QString(), ",").empty(),
"Invalid arguments was accepted");
QVERIFY2(QtCSV::Reader::readToList("./some/path.csv", ",").empty(),
"Invalid arguments was accepted");
QVERIFY2(
QtCSV::Reader::readToList(getPathToFileTestComma() + ".md5", ",").empty(),
"Invalid arguments was accepted");
}
void TestReader::testReadToDataInvalidArgs()
{
QtCSV::StringData data;
QVERIFY2(!QtCSV::Reader::readToData(QString(), data, QString()),
"Invalid arguments was accepted");
QVERIFY2(
!QtCSV::Reader::readToData(getPathToFileTestComma(), data, QString()),
"Invalid arguments was accepted");
QVERIFY2(!QtCSV::Reader::readToData(QString(), data, ","),
"Invalid arguments was accepted");
QVERIFY2(!QtCSV::Reader::readToData("./some/path.csv", data, ","),
"Invalid arguments was accepted");
QVERIFY2(
!QtCSV::Reader::readToData(getPathToFileTestComma() + ".md5", data, ","),
"Invalid arguments was accepted");
}
void TestReader::testReadFileWithCommas()
{
const auto path = getPathToFileTestComma();
const auto data = QtCSV::Reader::readToList(path);
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "one" << "two" << "three");
expected << (QList<QString>() << "one_element");
expected << (QList<QString>() << "1" << "2" << "3");
expected << (QList<QString>());
expected << (QList<QString>() << "3.14");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFileWithDotsInName()
{
const auto path = getPathToFileTestDotsInName();
const auto data = QtCSV::Reader::readToList(path);
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "one" << "two" << "three");
expected << (QList<QString>() << "one_element");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFileWithCommasToStringData()
{
const auto path = getPathToFileTestComma();
QtCSV::StringData strData;
const auto readResult = QtCSV::Reader::readToData(path, strData);
QVERIFY2(readResult, "Failed to read file content");
QVERIFY2(!strData.isEmpty(), "StringData is empty");
QList<QList<QString>> expected;
expected << (QList<QString>() << "one" << "two" << "three");
expected << (QList<QString>() << "one_element");
expected << (QList<QString>() << "1" << "2" << "3");
expected << (QList<QString>());
expected << (QList<QString>() << "3.14");
QVERIFY2(expected.size() == strData.rowCount(), "Wrong number of rows");
for (auto i = 0; i < strData.rowCount(); ++i)
{
QVERIFY2(expected.at(i) == strData.rowValues(i), "Wrong row data");
}
}
void TestReader::testReadFileWithCommasToVariantData()
{
const auto path = getPathToFileTestComma();
QtCSV::VariantData varData;
const auto readResult = QtCSV::Reader::readToData(path, varData);
QVERIFY2(readResult, "Failed to read file content");
QVERIFY2(!varData.isEmpty(), "StringData is empty");
QList<QList<QString>> expected;
expected << (QList<QString>() << "one" << "two" << "three");
expected << (QList<QString>() << "one_element");
expected << (QList<QString>() << "1" << "2" << "3");
expected << (QList<QString>());
expected << (QList<QString>() << "3.14");
QVERIFY2(expected.size() == varData.rowCount(), "Wrong number of rows");
for (auto i = 0; i < varData.rowCount(); ++i)
{
QVERIFY2(expected.at(i) == varData.rowValues(i), "Wrong row data");
}
}
void TestReader::testReadFileWithSemicolons()
{
const auto path = getPathToFileTestSemicolon();
const auto data = QtCSV::Reader::readToList(path, ";");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(2 == data.size(), "Wrong number of rows");
QList<QString> expectedFirstRow;
expectedFirstRow << "1" << "2" << "3";
QList<QString> expectedSecondRow;
expectedSecondRow << "j" << "io" << "pp";
QVERIFY2(expectedFirstRow == data.at(0), "Wrong first row");
QVERIFY2(expectedSecondRow == data.at(1), "Wrong second row");
}
void TestReader::testReadFileWithSemicolonsToStringData()
{
const auto path = getPathToFileTestSemicolon();
QtCSV::StringData strData;
const auto readResult = QtCSV::Reader::readToData(path, strData, ";");
QVERIFY2(readResult, "Failed to read file content");
QVERIFY2(!strData.isEmpty(), "Failed to read file content");
QVERIFY2(2 == strData.rowCount(), "Wrong number of rows");
QList<QString> expectedFirstRow;
expectedFirstRow << "1" << "2" << "3";
QList<QString> expectedSecondRow;
expectedSecondRow << "j" << "io" << "pp";
QVERIFY2(expectedFirstRow == strData.rowValues(0), "Wrong first row");
QVERIFY2(expectedSecondRow == strData.rowValues(1), "Wrong second row");
}
void TestReader::testReadFileWithSemicolonsToVariantData()
{
const auto path = getPathToFileTestSemicolon();
QtCSV::VariantData varData;
const auto readResult = QtCSV::Reader::readToData(path, varData, ";");
QVERIFY2(readResult, "Failed to read file content");
QVERIFY2(!varData.isEmpty(), "Failed to read file content");
QVERIFY2(2 == varData.rowCount(), "Wrong number of rows");
QList<QString> expectedFirstRow;
expectedFirstRow << "1" << "2" << "3";
QList<QString> expectedSecondRow;
expectedSecondRow << "j" << "io" << "pp";
QVERIFY2(expectedFirstRow == varData.rowValues(0), "Wrong first row");
QVERIFY2(expectedSecondRow == varData.rowValues(1), "Wrong second row");
}
void TestReader::testReadFileWithTextDelimDQoutes()
{
const auto path = getPathToFileTestDataTextDelimDQuotes();
const auto data = QtCSV::Reader::readToList(path, ",", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "one" << "two" << "three, four" << "five");
expected << (QList<QString>() << "this, is, one, element");
expected << (QList<QString>() << "six" << "seven" << "eight");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFileWithTextDelimQoutes()
{
const auto path = getPathToFileTestDataTextDelimQuotes();
const auto data = QtCSV::Reader::readToList(path, ",", "'");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "one" << "two" << "three, four" << "five");
expected << (QList<QString>() << "this, is, one, element");
expected << (QList<QString>() << "six" << "seven" << "eight");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFileWithTextDelimDQToStringData()
{
const auto path = getPathToFileTestDataTextDelimDQuotes();
QtCSV::StringData strData;
const auto readResult = QtCSV::Reader::readToData(path, strData, ",", "\"");
QVERIFY2(readResult, "Failed to read file content");
QVERIFY2(!strData.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "one" << "two" << "three, four" << "five");
expected << (QList<QString>() << "this, is, one, element");
expected << (QList<QString>() << "six" << "seven" << "eight");
QVERIFY2(expected.size() == strData.rowCount(), "Wrong number of rows");
for (auto i = 0; i < strData.rowCount(); ++i)
{
QVERIFY2(expected.at(i) == strData.rowValues(i), "Wrong row data");
}
}
void TestReader::testReadLongWithDQuotes()
{
const auto path = getPathToFileTestFieldWithDQuotes();
const auto data = QtCSV::Reader::readToList(path, ",", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "one" << "two" << "three");
expected << (QList<QString>() << "four" << "very \"long\" field" << "five");
expected << (QList<QString>() << "six" << "seven");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFieldWithCR()
{
const auto path = getPathToFileTestFieldWithCR();
const auto data = QtCSV::Reader::readToList(path, ";", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "Column1, Column2" << "Column3");
expected << (QList<QString>()
<< " Hello with\r Hello again " << "Hello Col 3");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
qWarning() << expected.at(i) << data.at(i);
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFieldWithCRLF()
{
const auto path = getPathToFileTestFieldWithCRLF();
const auto data = QtCSV::Reader::readToList(path, ",", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "one" << "two" << "three\nfour" << "five");
expected << (QList<QString>() << "six" << "seven");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFieldWithCRLFLong()
{
const auto path = getPathToFileTestFieldWithCRLFLong();
const auto data = QtCSV::Reader::readToList(path, ",", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>()
<< "one" << "two" << "three\nfour,\"five\n\"six\",seven\neight"
<< "nine");
expected << (QList<QString>() << "ten" << "eleven");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFieldEndTripleQuotes()
{
const auto path = getPathToFileTestFieldEndTripleQuotes();
const auto data = QtCSV::Reader::readToList(path, ",", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "CCLK=\"yy/MM/dd,hh:mm:ssA+zz\"" << "test");
expected << (QList<QString>() << "new" << "line \"it is\",\"def");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFileDataCorrectness()
{
const auto path = getPathToFileTestDataCorrectness();
const auto data = QtCSV::Reader::readToList(path, ",", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>()
<< "Year" << "Make" << "Model" << "Description" << "Price");
expected << (QList<QString>()
<< "1997" << "Ford" << "E350" << "ac, abs, moon" << "3000.00");
expected << (QList<QString>()
<< "1999" << "Chevy" << "Venture \"Extended Edition\"" << ""
<< "4900.00");
expected << (QList<QString>()
<< "1996" << "Jeep" << "Grand Cherokee"
<< "MUST SELL!\nair, moon roof, loaded" << "4799.00");
expected << (QList<QString>() << "1999" << "Chevy"
<< "Venture \"Extended Edition, Very Large\""
<< "" << "5000.00");
expected << (QList<QString>() << "" << "" << "Venture \"Extended Edition\""
<< "" << "4900.00");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFileWorldCitiesPop()
{
const auto path = getPathToFileWorldCitiesPop();
if (!QFile::exists(path))
{
qDebug()
<< "Skip testReadFileWorldCitiesPop() because file" << path
<< "do not exist. If you want to run this test, download file "
"from "
"http://www.maxmind.com/download/worldcities/worldcitiespop.txt.gz";
return;
}
QElapsedTimer timer;
timer.start();
const auto data = QtCSV::Reader::readToList(path, ",", "\"");
qDebug() << "Elapsed time:" << timer.elapsed() << "ms";
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(3173959 == data.size(), "Wrong number of rows");
}
void TestReader::testReadFileWithEmptyFields()
{
const auto path = getPathToFileWithEmptyFields();
const auto data = QtCSV::Reader::readToList(path, ",", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << QString() << "0" << QString() << QString());
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFileWithEmptyFieldsComplexSeparator()
{
const auto path = getPathToFileWithEmptyFieldsComplexSeparator();
const auto data = QtCSV::Reader::readToList(path, ";-;", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "0" << "1" << QString() << QString());
expected << (QList<QString>()
<< QString() << "Question" << "158" << QString() << QString());
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadFileWithMultirowData()
{
const auto path = getPathToFileMultirowData();
const auto data = QtCSV::Reader::readToList(path);
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "A" << "B" << "C" << "D");
expected << (QList<QString>() << "a" << "b" << "c" << "d");
expected << (QList<QString>() << "a" << "b field" << "c field\n"
<< "d field");
expected << (QList<QString>() << "aa" << "bb" << "cc" << "dd");
QVERIFY2(expected.size() == data.size(), "Wrong number of rows");
for (auto i = 0; i < data.size(); ++i)
{
QVERIFY2(expected.at(i) == data.at(i), "Wrong row data");
}
}
void TestReader::testReadByProcessorWithBreak()
{
class ProcessorWithBreak : public QtCSV::Reader::AbstractProcessor
{
public:
size_t counter = 0;
QList<QList<QString>> data;
bool processRowElements(const QList<QString> &elements) override
{
if (counter < 2)
{
data << elements;
++counter;
}
return true;
}
};
const auto path = getPathToFileMultirowData();
ProcessorWithBreak processor;
QVERIFY2(QtCSV::Reader::readToProcessor(path, processor),
"Failed to read file content");
QList<QList<QString>> expected;
expected << (QList<QString>() << "A" << "B" << "C" << "D");
expected << (QList<QString>() << "a" << "b" << "c" << "d");
QVERIFY2(expected.size() == processor.data.size(), "Wrong number of rows");
for (auto i = 0; i < processor.data.size(); ++i)
{
QVERIFY2(expected.at(i) == processor.data.at(i), "Wrong row data");
}
}
QString TestReader::getPathToFolderWithTestFiles() const
{
return QDir::currentPath() + "/data/";
}
QString TestReader::getPathToFileTestComma() const
{
return getPathToFolderWithTestFiles() + "test-comma.csv";
}
QString TestReader::getPathToFileTestDotsInName() const
{
return getPathToFolderWithTestFiles() + "test.file.dots.csv";
}
QString TestReader::getPathToFileTestSemicolon() const
{
return getPathToFolderWithTestFiles() + "test-semicolon.csv";
}
QString TestReader::getPathToFileTestDataTextDelimDQuotes() const
{
return getPathToFolderWithTestFiles()
+ "test-data-text-delim-double-quotes.csv";
}
QString TestReader::getPathToFileTestDataTextDelimQuotes() const
{
return getPathToFolderWithTestFiles() + "test-data-text-delim-quotes.csv";
}
QString TestReader::getPathToFileTestFieldWithDQuotes() const
{
return getPathToFolderWithTestFiles() + "test-field-with-dquotes.csv";
}
QString TestReader::getPathToFileTestFieldWithCR() const
{
return getPathToFolderWithTestFiles() + "test-field-with-cr.csv";
}
QString TestReader::getPathToFileTestFieldWithCRLF() const
{
return getPathToFolderWithTestFiles() + "test-field-with-crlf.csv";
}
QString TestReader::getPathToFileTestFieldWithCRLFLong() const
{
return getPathToFolderWithTestFiles() + "test-field-with-crlf-long.csv";
}
QString TestReader::getPathToFileTestFieldEndTripleQuotes() const
{
return getPathToFolderWithTestFiles() + "test-field-end-triple-quotes.csv";
}
QString TestReader::getPathToFileTestDataCorrectness() const
{
return getPathToFolderWithTestFiles() + "test-data-correctness.csv";
}
QString TestReader::getPathToFileWorldCitiesPop() const
{
return getPathToFolderWithTestFiles() + "worldcitiespop.txt";
}
QString TestReader::getPathToFileWithEmptyFields() const
{
return getPathToFolderWithTestFiles() + "test-empty-fields.csv";
}
QString TestReader::getPathToFileWithEmptyFieldsComplexSeparator() const
{
return getPathToFolderWithTestFiles()
+ "test-empty-fields-complex-separator.csv";
}
QString TestReader::getPathToFileMultirowData() const
{
return getPathToFolderWithTestFiles() + "test-multirow-data.csv";
}

View File

@ -0,0 +1,58 @@
#ifndef TESTREADER_H
#define TESTREADER_H
#include <QObject>
#include <QtTest>
class TestReader : public QObject
{
Q_OBJECT
public:
TestReader() = default;
private Q_SLOTS:
void testReadToListInvalidArgs();
void testReadToDataInvalidArgs();
void testReadFileWithCommas();
void testReadFileWithDotsInName();
void testReadFileWithCommasToStringData();
void testReadFileWithCommasToVariantData();
void testReadFileWithSemicolons();
void testReadFileWithSemicolonsToStringData();
void testReadFileWithSemicolonsToVariantData();
void testReadFileWithTextDelimDQoutes();
void testReadFileWithTextDelimQoutes();
void testReadFileWithTextDelimDQToStringData();
void testReadLongWithDQuotes();
void testReadFieldWithCR();
void testReadFieldWithCRLF();
void testReadFieldWithCRLFLong();
void testReadFieldEndTripleQuotes();
void testReadFileDataCorrectness();
void testReadFileWorldCitiesPop();
void testReadFileWithEmptyFields();
void testReadFileWithEmptyFieldsComplexSeparator();
void testReadFileWithMultirowData();
void testReadByProcessorWithBreak();
private:
QString getPathToFolderWithTestFiles() const;
QString getPathToFileTestComma() const;
QString getPathToFileTestDotsInName() const;
QString getPathToFileTestSemicolon() const;
QString getPathToFileTestDataTextDelimDQuotes() const;
QString getPathToFileTestDataTextDelimQuotes() const;
QString getPathToFileTestFieldWithDQuotes() const;
QString getPathToFileTestFieldWithCR() const;
QString getPathToFileTestFieldWithCRLF() const;
QString getPathToFileTestFieldWithCRLFLong() const;
QString getPathToFileTestFieldEndTripleQuotes() const;
QString getPathToFileTestDataCorrectness() const;
QString getPathToFileWorldCitiesPop() const;
QString getPathToFileWithEmptyFields() const;
QString getPathToFileWithEmptyFieldsComplexSeparator() const;
QString getPathToFileMultirowData() const;
};
#endif // TESTREADER_H

76
lib/qtcsv/tests/tests.pro Normal file
View File

@ -0,0 +1,76 @@
QT += testlib
QT -= gui
TARGET = qtcsv_tests
CONFIG += console testcase
CONFIG -= app_bundle
TEMPLATE = app
!msvc {
# flags for gcc-like compiler
CONFIG += warn_on
QMAKE_CXXFLAGS_WARN_ON += -Werror -Wformat=2 -Wuninitialized -Winit-self \
-Wmissing-include-dirs -Wswitch-enum -Wundef -Wpointer-arith \
-Wdisabled-optimization -Wcast-align -Wcast-qual
}
# set where linker could find qtcsv library. By default we expect
# that library is located in the same directory as the qtcsv_tests binary.
QTCSV_LOCATION = $$OUT_PWD
LIBS += -L$$QTCSV_LOCATION -lqtcsv
# Uncomment this settings if you want to manually set destination directory for
# compiled binary
#CONFIG(release, debug|release): DESTDIR = $$PWD/../
#CONFIG(debug, debug|release): DESTDIR = $$PWD/../
INCLUDEPATH += $$PWD/../include
SOURCES += \
tst_testmain.cpp \
teststringdata.cpp \
testvariantdata.cpp \
testreader.cpp \
testwriter.cpp
HEADERS += \
teststringdata.h \
testvariantdata.h \
testreader.h \
testwriter.h
DISTFILES += \
CMakeLists.txt
!equals(PWD, $$OUT_PWD){
# Copy 'data' folder with test files to the destination directory
win32 {
COPY_FROM_PATH=$$shell_path($$PWD/data)
COPY_TO_PATH=$$shell_path($$OUT_PWD/data)
# on windows we should create "/data" directory before coping of files
createdir.commands = $(MKDIR) $$COPY_TO_PATH
first.depends = $(first) createdir
}
else {
COPY_FROM_PATH=$$PWD/data
COPY_TO_PATH=$$OUT_PWD
}
copydata.commands = $(COPY_DIR) $$COPY_FROM_PATH $$COPY_TO_PATH
first.depends = $(first) copydata
export(first.depends)
win32: export(createdir.commands)
export(copydata.commands)
win32: QMAKE_EXTRA_TARGETS += first createdir copydata
else: QMAKE_EXTRA_TARGETS += first copydata
}
message(=== Configuration of qtcsv_tests ===)
message(Qt version: $$[QT_VERSION])
message(qtcsv_tests binary will be created in folder: $$OUT_PWD)
message(Expected location of qtcsv library: $$QTCSV_LOCATION)
message(Expected location of \"data\" folder with test files: $$OUT_PWD)

View File

@ -0,0 +1,277 @@
#include "teststringdata.h"
#include "qtcsv/stringdata.h"
#include <QDebug>
#include <QTest>
void TestStringData::testCreation()
{
QtCSV::StringData strData;
QVERIFY2(strData.isEmpty(), "Empty StringData is not empty");
QVERIFY2(strData.rowCount() == 0, "Empty StringData have too many rows");
}
void TestStringData::testAddEmptyRow()
{
QtCSV::StringData strData;
strData.addEmptyRow();
QVERIFY2(!strData.isEmpty(), "StringData is empty with empty line");
QVERIFY2(1 == strData.rowCount(), "Wrong number of rows");
QVERIFY2(QList<QString>() == strData.rowValues(0),
"Wrong data for empty row");
}
void TestStringData::testAddOneRow()
{
QList<QString> rowValues;
rowValues << "one" << "two" << "three";
QtCSV::StringData strData;
strData.addRow(rowValues);
QVERIFY2(!strData.isEmpty(), "StringData is empty");
QVERIFY2(1 == strData.rowCount(), "Wrong number of rows");
QVERIFY2(rowValues == strData.rowValues(0), "Wrong data for empty row");
}
void TestStringData::testAddOneRowUsingOneString()
{
QString value("faklj;");
QtCSV::StringData strData;
strData.addRow(value);
QVERIFY2(!strData.isEmpty(), "StringData is empty");
QVERIFY2(1 == strData.rowCount(), "Wrong number of rows");
QList<QString> expectedRow;
expectedRow << value;
QVERIFY2(expectedRow == strData.rowValues(0), "Wrong data for empty row");
}
void TestStringData::testAddRows()
{
QList<QString> valuesFirst;
valuesFirst << "1" << "2" << "3";
QList<QString> valuesSecond;
QList<QString> valuesThird;
valuesFirst << "hhh" << "ttyyeeqp[" << "n...589129";
QtCSV::StringData strData;
strData.addRow(valuesFirst);
strData.addRow(valuesSecond);
strData.addRow(valuesThird);
QVERIFY2(!strData.isEmpty(), "StringData is empty");
QVERIFY2(3 == strData.rowCount(), "Wrong number of rows");
QVERIFY2(valuesFirst == strData.rowValues(0), "Wrong data for first row");
QVERIFY2(valuesSecond == strData.rowValues(1), "Wrong data for second row");
QVERIFY2(valuesThird == strData.rowValues(2), "Wrong data for third row");
}
void TestStringData::testClearEmptyData()
{
QtCSV::StringData strData;
QVERIFY2(strData.isEmpty(), "StringData is not empty");
strData.clear();
QVERIFY2(strData.isEmpty(), "StringData is not empty");
}
void TestStringData::testClearNotEmptyData()
{
QList<QString> rowValues;
rowValues << "one" << "two" << "three";
QtCSV::StringData strData;
strData.addRow(rowValues);
QVERIFY2(!strData.isEmpty(), "StringData is empty");
strData.clear();
QVERIFY2(strData.isEmpty(), "StringData is not empty");
}
void TestStringData::testInsertRows()
{
QList<QString> valuesFirst, valuesSecond;
valuesFirst << "one" << "two" << "three";
valuesSecond << "asgreg" << "ertetw" << "";
QString stringOne("hey test"), stringTwo("sdfwioiouoioi");
QtCSV::StringData strData;
strData.insertRow(0, valuesFirst);
QVERIFY2(1 == strData.rowCount(), "Wrong number of rows");
QVERIFY2(valuesFirst == strData.rowValues(0), "Wrong data for first row");
strData.addEmptyRow();
strData.addRow(stringOne);
strData.insertRow(1, valuesSecond);
strData.insertRow(100, stringTwo);
QVERIFY2(5 == strData.rowCount(), "Wrong number of rows");
QVERIFY2(valuesFirst == strData.rowValues(0), "Wrong data for first row");
QVERIFY2(valuesSecond == strData.rowValues(1), "Wrong data for second row");
QVERIFY2(strData.rowValues(2).isEmpty(), "Wrong data for third row");
QVERIFY2((QList<QString>() << stringOne) == strData.rowValues(3),
"Wrong data for fourth row");
QVERIFY2((QList<QString>() << stringTwo) == strData.rowValues(4),
"Wrong data for fifth row");
}
void TestStringData::testCompareForEquality()
{
QList<QString> firstRow, secondRow;
firstRow << "one" << "two" << "three";
secondRow << "four" << "five";
QtCSV::StringData firstData;
firstData.addRow(firstRow);
firstData.addRow(secondRow);
QtCSV::StringData secondData;
secondData.addRow(firstRow);
secondData.addRow(secondRow);
QVERIFY2(firstData == firstData,
"Failed to compare for equality same object");
QVERIFY2(!(firstData != firstData),
"Failed to compare for equality same object");
QVERIFY2(firstData == secondData, "Objects are not the same");
QVERIFY2(!(firstData != secondData), "Objects are not the same");
secondData.addRow(firstRow);
QVERIFY2(!(firstData == secondData), "Objects are the same");
QVERIFY2(firstData != secondData, "Objects are the same");
}
void TestStringData::testCopyConstruction()
{
QList<QString> firstRow, secondRow;
firstRow << "one" << "two" << "three";
secondRow << "four" << "five";
QtCSV::StringData firstData;
firstData.addRow(firstRow);
firstData.addRow(secondRow);
{
QtCSV::StringData secondData(firstData);
QVERIFY2(firstData.rowCount() == secondData.rowCount(),
"Wrong number of rows");
QVERIFY2(firstRow == secondData.rowValues(0), "Wrong data for first row");
QVERIFY2(secondRow == secondData.rowValues(1), "Wrong data for second row");
}
QVERIFY2(2 == firstData.rowCount(), "Wrong number of rows");
QVERIFY2(firstRow == firstData.rowValues(0), "Wrong data for first row");
QVERIFY2(secondRow == firstData.rowValues(1), "Wrong data for second row");
}
void TestStringData::testCopyAssignment()
{
QList<QString> firstRow, secondRow;
firstRow << "one" << "two" << "three";
secondRow << "four" << "five";
QtCSV::StringData firstData;
firstData.addRow(firstRow);
firstData.addRow(secondRow);
{
QtCSV::StringData secondData;
secondData = firstData;
QVERIFY2(firstData.rowCount() == secondData.rowCount(),
"Wrong number of rows");
QVERIFY2(firstRow == secondData.rowValues(0), "Wrong data for first row");
QVERIFY2(secondRow == secondData.rowValues(1), "Wrong data for second row");
}
QVERIFY2(2 == firstData.rowCount(), "Wrong number of rows");
QVERIFY2(firstRow == firstData.rowValues(0), "Wrong data for first row");
QVERIFY2(secondRow == firstData.rowValues(1), "Wrong data for second row");
}
void TestStringData::testOperatorInput()
{
QtCSV::StringData data;
data << QString("1") << "one";
QList<QString> thirdRow;
thirdRow << "one" << "two" << "three";
data << thirdRow;
QVERIFY2(3 == data.rowCount(), "Wrong number of rows");
QList<QString> expectedFirstRow, expectedSecondRow;
expectedFirstRow << "1";
expectedSecondRow << "one";
QVERIFY2(expectedFirstRow == data.rowValues(0), "Wrong data for first row");
QVERIFY2(expectedSecondRow == data.rowValues(1), "Wrong data for second row");
QVERIFY2(thirdRow == data.rowValues(2), "Wrong data for third row");
}
void TestStringData::testRemoveRow()
{
QtCSV::StringData strData;
strData.removeRow(0);
strData.removeRow(563);
QVERIFY2(strData.isEmpty(), "Container is not empty");
QList<QString> valuesFirst, valuesSecond;
valuesFirst << "one" << "two" << "three";
valuesSecond << "asgreg" << "ertetw" << "";
QString stringOne("hey test"), stringTwo("sdfwioiouoioi");
strData << valuesFirst << valuesSecond << stringOne << stringTwo;
strData.removeRow(2);
QVERIFY2(3 == strData.rowCount(), "Wrong number of rows");
QVERIFY2(valuesFirst == strData.rowValues(0), "Wrong data for first row");
QVERIFY2(valuesSecond == strData.rowValues(1), "Wrong data for second row");
QVERIFY2((QList<QString>() << stringTwo) == strData.rowValues(2),
"Wrong data for third row");
}
void TestStringData::testReplaceRow()
{
QList<QString> valuesFirst, valuesSecond;
valuesFirst << "one" << "two" << "three";
valuesSecond << "asgreg" << "ertetw" << "";
QString stringOne("hey test"), stringTwo("sdfwioiouoioi");
QtCSV::StringData strData;
strData << valuesFirst << valuesSecond << stringOne;
strData.replaceRow(0, stringTwo);
QVERIFY2((QList<QString>() << stringTwo) == strData.rowValues(0),
"Wrong data for first row");
strData.replaceRow(2, QList<QString>());
QVERIFY2(QList<QString>() == strData.rowValues(2),
"Wrong data for third row");
strData.replaceRow(1, valuesFirst);
QVERIFY2(valuesFirst == strData.rowValues(1), "Wrong data for second row");
}

View File

@ -0,0 +1,30 @@
#ifndef TESTSTRINGDATA_H
#define TESTSTRINGDATA_H
#include <QtTest>
class TestStringData : public QObject
{
Q_OBJECT
public:
TestStringData() = default;
private Q_SLOTS:
void testCreation();
void testAddEmptyRow();
void testAddOneRow();
void testAddOneRowUsingOneString();
void testAddRows();
void testClearEmptyData();
void testClearNotEmptyData();
void testInsertRows();
void testCompareForEquality();
void testCopyConstruction();
void testCopyAssignment();
void testOperatorInput();
void testRemoveRow();
void testReplaceRow();
};
#endif // TESTSTRINGDATA_H

View File

@ -0,0 +1,329 @@
#include "testvariantdata.h"
#include "qtcsv/variantdata.h"
const double EPSILON = 0.000001;
void TestVariantData::testCreation()
{
QtCSV::VariantData varData;
QVERIFY2(varData.isEmpty(), "Empty VariantData is not empty");
QVERIFY2(varData.rowCount() == 0, "Empty VariantData have too many rows");
}
void TestVariantData::testAddEmptyRow()
{
QtCSV::VariantData varData;
varData.addEmptyRow();
QVERIFY2(!varData.isEmpty(), "VariantData is empty with empty line");
QVERIFY2(1 == varData.rowCount(), "Wrong number of rows");
QVERIFY2(QStringList() == varData.rowValues(0), "Wrong data for empty row");
}
void TestVariantData::testAddOneRow()
{
QList<QVariant> values;
QString strValue("Hey");
int intValue = 7000;
values << strValue << intValue;
QtCSV::VariantData varData;
varData.addRow(values);
QVERIFY2(!varData.isEmpty(), "VariantData is empty");
QVERIFY2(1 == varData.rowCount(), "Wrong number of rows");
QStringList resultValues = varData.rowValues(0);
QVERIFY2(2 == resultValues.size(), "Wrong number of values");
auto resultInt = resultValues.at(1).toInt();
QVERIFY2(strValue == resultValues.at(0), "Wrong string data");
QVERIFY2(resultInt == intValue, "Wrong int data");
}
void TestVariantData::testAddOneRowUsingOneElement()
{
int expectedValue = 42;
QVariant varValue(expectedValue);
QtCSV::VariantData varData;
varData.addRow(varValue);
QVERIFY2(!varData.isEmpty(), "VariantData is empty");
QVERIFY2(1 == varData.rowCount(), "Wrong number of rows");
QStringList values = varData.rowValues(0);
QVERIFY2(1 == values.size(), "Wrong number of values");
auto resultValue = values.at(0).toInt();
QVERIFY2(resultValue == expectedValue, "Wrong data");
}
void TestVariantData::testAddRows()
{
double expectedValue = 42.12309;
QVariant firstRow(expectedValue);
QList<QVariant> secondRow;
secondRow << QVariant("kkoo") << QVariant(771) << QVariant(3.14);
QStringList thirdRow;
thirdRow << "one" << "two" << "three";
QtCSV::VariantData varData;
varData.addRow(firstRow);
varData.addRow(secondRow);
varData.addRow(thirdRow);
QVERIFY2(!varData.isEmpty(), "VariantData is empty");
QVERIFY2(3 == varData.rowCount(), "Wrong number of rows");
QStringList values = varData.rowValues(0);
QVERIFY2(1 == values.size(), "Wrong number of values for first row");
auto resultValue = values.at(0).toDouble();
QVERIFY2(resultValue - expectedValue < EPSILON, "Wrong double value");
QStringList secondRowValues = varData.rowValues(1);
QVERIFY2(secondRow.size() == secondRowValues.size(),
"Wrong number of elements in second row");
QVERIFY2(secondRow.at(0).toString() == secondRowValues.at(0),
"Wrong first element in second row");
QVERIFY2(secondRow.at(1).toInt() == secondRowValues.at(1).toInt(),
"Wrong second element in second row");
auto diff = secondRow.at(2).toDouble() - secondRowValues.at(2).toDouble();
QVERIFY2(diff <= EPSILON, "Wrong third element in second row");
QVERIFY2(thirdRow == varData.rowValues(2), "Wrong third row values");
}
void TestVariantData::testClearEmptyData()
{
QtCSV::VariantData varData;
QVERIFY2(varData.isEmpty(), "VariantData is not empty");
QVERIFY2(0 == varData.rowCount(), "Wrong number of rows");
varData.clear();
QVERIFY2(varData.isEmpty(), "VariantData is not empty");
QVERIFY2(0 == varData.rowCount(), "Wrong number of rows");
}
void TestVariantData::testClearNotEmptyData()
{
double expectedValue = 42.12309;
QVariant firstRow(expectedValue);
QList<QVariant> secondRow;
secondRow << QVariant("kkoo") << QVariant(771) << QVariant(3.14);
QStringList thirdRow;
thirdRow << "one" << "two" << "three";
QtCSV::VariantData varData;
varData.addRow(firstRow);
varData.addRow(secondRow);
varData.addRow(thirdRow);
QVERIFY2(!varData.isEmpty(), "VariantData is empty");
QVERIFY2(3 == varData.rowCount(), "Wrong number of rows");
varData.clear();
QVERIFY2(varData.isEmpty(), "VariantData is not empty");
QVERIFY2(0 == varData.rowCount(), "Wrong number of rows");
}
void TestVariantData::testInsertRows()
{
double expectedValue = 42.12309;
QVariant firstRow(expectedValue);
QList<QVariant> secondRow;
secondRow << QVariant("kkoo") << QVariant(771) << QVariant(3.14);
QStringList thirdRow;
thirdRow << "one" << "two" << "three";
QString stringOne("hey testing");
QtCSV::VariantData varData;
varData.insertRow(0, firstRow);
QVERIFY2(1 == varData.rowCount(), "Wrong number of rows");
QVERIFY2(1 == varData.rowValues(0).size(),
"Wrong number of elements in first row");
auto diff = expectedValue - varData.rowValues(0).at(0).toDouble();
QVERIFY2(diff <= EPSILON, "Wrong number in first row");
varData.addEmptyRow();
varData.addRow(stringOne);
varData.insertRow(1, secondRow);
varData.insertRow(100, thirdRow);
QVERIFY2(5 == varData.rowCount(), "Wrong number of rows");
diff = expectedValue - varData.rowValues(0).at(0).toDouble();
QVERIFY2(diff <= EPSILON, "Wrong data for first row");
QStringList resultSecondRow = varData.rowValues(1);
QVERIFY2(secondRow.at(0).toString() == resultSecondRow.at(0)
&& secondRow.at(1).toString() == resultSecondRow.at(1)
&& secondRow.at(2).toString() == resultSecondRow.at(2),
"Wrong data for second row");
QVERIFY2(QStringList() == varData.rowValues(2), "Wrong data for third row");
QVERIFY2((QStringList() << stringOne) == varData.rowValues(3),
"Wrong data for fourth row");
QVERIFY2(thirdRow == varData.rowValues(4), "Wrong data for fifth row");
}
void TestVariantData::testCompareForEquality()
{
QStringList firstRow, secondRow;
firstRow << "one" << "two" << "three";
secondRow << "four" << "five";
QtCSV::VariantData firstData;
firstData.addRow(firstRow);
firstData.addRow(secondRow);
QtCSV::VariantData secondData;
secondData.addRow(firstRow);
secondData.addRow(secondRow);
QVERIFY2(firstData == firstData,
"Failed to compare for equality same object");
QVERIFY2(!(firstData != firstData),
"Failed to compare for equality same object");
QVERIFY2(firstData == secondData, "Objects are not the same");
QVERIFY2(!(firstData != secondData), "Objects are not the same");
secondData.addRow(firstRow);
QVERIFY2(!(firstData == secondData), "Objects are the same");
QVERIFY2(firstData != secondData, "Objects are the same");
}
void TestVariantData::testCopyConstruction()
{
QStringList firstRow, secondRow;
firstRow << "one" << "two" << "three";
secondRow << "four" << "five";
QtCSV::VariantData firstData;
firstData.addRow(firstRow);
firstData.addRow(secondRow);
{
QtCSV::VariantData secondData(firstData);
QVERIFY2(firstData.rowCount() == secondData.rowCount(),
"Wrong number of rows");
QVERIFY2(firstRow == secondData.rowValues(0), "Wrong data for first row");
QVERIFY2(secondRow == secondData.rowValues(1), "Wrong data for second row");
}
QVERIFY2(2 == firstData.rowCount(), "Wrong number of rows");
QVERIFY2(firstRow == firstData.rowValues(0), "Wrong data for first row");
QVERIFY2(secondRow == firstData.rowValues(1), "Wrong data for second row");
}
void TestVariantData::testCopyAssignment()
{
QStringList firstRow, secondRow;
firstRow << "one" << "two" << "three";
secondRow << "four" << "five";
QtCSV::VariantData firstData;
firstData.addRow(firstRow);
firstData.addRow(secondRow);
{
QtCSV::VariantData secondData;
secondData = firstData;
QVERIFY2(firstData.rowCount() == secondData.rowCount(),
"Wrong number of rows");
QVERIFY2(firstRow == secondData.rowValues(0), "Wrong data for first row");
QVERIFY2(secondRow == secondData.rowValues(1), "Wrong data for second row");
}
QVERIFY2(2 == firstData.rowCount(), "Wrong number of rows");
QVERIFY2(firstRow == firstData.rowValues(0), "Wrong data for first row");
QVERIFY2(secondRow == firstData.rowValues(1), "Wrong data for second row");
}
void TestVariantData::testOperatorInput()
{
QtCSV::VariantData data;
data << QString("1") << QVariant(double(3.14));
QStringList thirdRow;
thirdRow << "one" << "two" << "three";
data << thirdRow;
QVERIFY2(3 == data.rowCount(), "Wrong number of rows");
QStringList expectedFirstRow, expectedSecondRow;
expectedFirstRow << "1";
expectedSecondRow << QVariant(double(3.14)).toString();
QVERIFY2(expectedFirstRow == data.rowValues(0), "Wrong data for first row");
QVERIFY2(expectedSecondRow == data.rowValues(1), "Wrong data for second row");
QVERIFY2(thirdRow == data.rowValues(2), "Wrong data for third row");
}
void TestVariantData::testRemoveRow()
{
QtCSV::VariantData data;
data.removeRow(0);
data.removeRow(563);
QVERIFY2(data.isEmpty(), "Container is not empty");
QStringList valuesFirst, valuesSecond;
valuesFirst << "one" << "two" << "three";
valuesSecond << "asgreg" << "ertetw" << "";
QString stringOne("hey test"), stringTwo("sdfwioiouoioi");
data << valuesFirst << valuesSecond << stringOne << stringTwo;
data.removeRow(2);
QVERIFY2(3 == data.rowCount(), "Wrong number of rows");
QVERIFY2(valuesFirst == data.rowValues(0), "Wrong data for first row");
QVERIFY2(valuesSecond == data.rowValues(1), "Wrong data for second row");
QVERIFY2((QStringList() << stringTwo) == data.rowValues(2),
"Wrong data for third row");
}
void TestVariantData::testReplaceRow()
{
QStringList valuesFirst, valuesSecond;
valuesFirst << "one" << "two" << "three";
valuesSecond << "asgreg" << "ertetw" << "";
QString stringOne("hey test"), stringTwo("sdfwioiouoioi");
QtCSV::VariantData data;
data << valuesFirst << valuesSecond << stringOne;
data.replaceRow(0, stringTwo);
QVERIFY2((QStringList() << stringTwo) == data.rowValues(0),
"Wrong data for first row");
data.replaceRow(2, QStringList());
QVERIFY2(QStringList() == data.rowValues(2), "Wrong data for third row");
data.replaceRow(1, valuesFirst);
QVERIFY2(valuesFirst == data.rowValues(1), "Wrong data for second row");
}

View File

@ -0,0 +1,31 @@
#ifndef TESTVARIANTDATA_H
#define TESTVARIANTDATA_H
#include <QObject>
#include <QtTest>
class TestVariantData : public QObject
{
Q_OBJECT
public:
TestVariantData() = default;
private Q_SLOTS:
void testCreation();
void testAddEmptyRow();
void testAddOneRow();
void testAddOneRowUsingOneElement();
void testAddRows();
void testClearEmptyData();
void testClearNotEmptyData();
void testInsertRows();
void testCompareForEquality();
void testCopyConstruction();
void testCopyAssignment();
void testOperatorInput();
void testRemoveRow();
void testReplaceRow();
};
#endif // TESTVARIANTDATA_H

View File

@ -0,0 +1,406 @@
#include "testwriter.h"
#include "qtcsv/reader.h"
#include "qtcsv/variantdata.h"
#include "qtcsv/writer.h"
#include <QDebug>
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <exception>
void TestWriter::cleanup()
{
if (QFile::exists(getFilePath()) && !QFile::remove(getFilePath()))
{
qDebug() << "Can't remove file:" << getFilePath();
}
if (QFile::exists(getFilePathXLS()) && !QFile::remove(getFilePathXLS()))
{
qDebug() << "Can't remove file:" << getFilePathXLS();
}
if (QFile::exists(getFilePathWithDotsInName())
&& !QFile::remove(getFilePathWithDotsInName()))
{
qDebug() << "Can't remove file:" << getFilePathWithDotsInName();
}
}
QString TestWriter::getFilePath() const
{
return QDir::currentPath() + "/test-file.csv";
}
QString TestWriter::getFilePathXLS() const
{
return QDir::currentPath() + "/test-file.xls";
}
QString TestWriter::getFilePathWithDotsInName() const
{
return QDir::currentPath() + "/test.file.dots.csv";
}
void TestWriter::testWriteInvalidArgs()
{
QVERIFY2(!QtCSV::Writer::write(QString(), QtCSV::StringData()),
"Invalid arguments was accepted");
QVERIFY2(!QtCSV::Writer::write(getFilePath(), QtCSV::StringData()),
"Empty data was accepted");
QtCSV::StringData strData;
strData << "one" << "two" << "three";
QVERIFY2(!QtCSV::Writer::write(QString(), strData),
"Empty path was accepted");
QVERIFY2(!QtCSV::Writer::write("./some/path.csv", strData),
"Relative path to csv-file was accepted");
QVERIFY2(QtCSV::Writer::write(getFilePathXLS(), strData),
"Absolute path to xls-file was not accepted");
}
void TestWriter::testWriteFromStringData()
{
QList<QString> strList;
strList << "one" << "two" << "three";
QtCSV::StringData strData;
strData.addRow(strList);
const auto writeResult = QtCSV::Writer::write(getFilePath(), strData);
QVERIFY2(writeResult, "Failed to write to file");
const auto data = QtCSV::Reader::readToList(getFilePath());
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(1 == data.size(), "Wrong number of rows");
QVERIFY2(strList == data.at(0), "Wrong data");
}
void TestWriter::testWriteFromVariantData()
{
const auto expectedValue = 42.12309;
QVariant firstRow(expectedValue);
QList<QVariant> secondRow;
secondRow << QVariant("kkoo") << QVariant(771) << QVariant(3.14);
QList<QString> thirdRow;
thirdRow << "one" << "two" << "three";
QtCSV::VariantData varData;
varData.addRow(firstRow);
varData.addRow(secondRow);
varData.addRow(thirdRow);
const auto writeResult = QtCSV::Writer::write(getFilePath(), varData);
QVERIFY2(writeResult, "Failed to write to file");
const auto data = QtCSV::Reader::readToList(getFilePath());
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(3 == data.size(), "Wrong number of rows");
QVERIFY2(varData.rowValues(0) == data.at(0), "Wrong values in first row");
QVERIFY2(varData.rowValues(1) == data.at(1), "Wrong values in second row");
QVERIFY2(varData.rowValues(2) == data.at(2), "Wrong values in third row");
}
void TestWriter::testWriteToFileWithDotsInName()
{
QList<QString> strList;
strList << "one" << "two" << "three";
QtCSV::StringData strData;
strData.addRow(strList);
const auto writeResult
= QtCSV::Writer::write(getFilePathWithDotsInName(), strData);
QVERIFY2(writeResult, "Failed to write to file");
const auto data = QtCSV::Reader::readToList(getFilePathWithDotsInName());
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(1 == data.size(), "Wrong number of rows");
QVERIFY2(strList == data.at(0), "Wrong data");
}
void TestWriter::testWriteAppendMode()
{
QList<QString> strFirstList;
strFirstList << "one" << "two" << "three";
QtCSV::StringData strData;
strData.addRow(strFirstList);
const auto writeResult = QtCSV::Writer::write(getFilePath(), strData);
QVERIFY2(writeResult, "Failed to write to file");
QList<QString> strSecondList;
strSecondList << "3" << "2" << "1.1";
QtCSV::StringData newStrData;
newStrData.addRow(strSecondList);
const auto newWriteResult = QtCSV::Writer::write(
getFilePath(), newStrData, ",", {}, QtCSV::Writer::WriteMode::APPEND);
QVERIFY2(newWriteResult, "Failed to write to file");
const auto data = QtCSV::Reader::readToList(getFilePath());
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(2 == data.size(), "Wrong number of rows");
QVERIFY2(strFirstList == data.at(0), "Wrong first row data");
QVERIFY2(strSecondList == data.at(1), "Wrong second row data");
}
void TestWriter::testWriteWithNotDefaultSeparator()
{
QList<QString> strList;
strList << "one" << "two" << "three";
QtCSV::StringData strData;
strData.addRow(strList);
const QString separator("++");
const auto writeResult
= QtCSV::Writer::write(getFilePath(), strData, separator);
QVERIFY2(writeResult, "Failed to write to file");
const auto data = QtCSV::Reader::readToList(getFilePath(), separator);
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(1 == data.size(), "Wrong number of rows");
QVERIFY2(strList == data.at(0), "Wrong data");
}
void TestWriter::testWriteWithHeader()
{
QList<QString> header;
header << "1" << "2";
QList<QString> strList;
strList << "one" << "two" << "three";
QtCSV::StringData strData;
strData.addRow(strList);
const auto writeResult
= QtCSV::Writer::write(getFilePath(), strData, ",", {},
QtCSV::Writer::WriteMode::REWRITE, header);
QVERIFY2(writeResult, "Failed to write to file");
const auto data = QtCSV::Reader::readToList(getFilePath());
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(2 == data.size(), "Wrong number of rows");
QVERIFY2(header == data.at(0), "Wrong header");
QVERIFY2(strList == data.at(1), "Wrong data");
}
void TestWriter::testWriteWithFooter()
{
QList<QString> footer;
footer << "Here is a footer";
QList<QString> strList;
strList << "one" << "two" << "three";
QtCSV::StringData strData;
strData.addRow(strList);
const auto writeResult
= QtCSV::Writer::write(getFilePath(), strData, ",", {},
QtCSV::Writer::WriteMode::REWRITE, {}, footer);
QVERIFY2(writeResult, "Failed to write to file");
const auto data = QtCSV::Reader::readToList(getFilePath());
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(2 == data.size(), "Wrong number of rows");
QVERIFY2(strList == data.at(0), "Wrong data");
QVERIFY2(footer == data.at(1), "Wrong footer");
}
void TestWriter::testWriteWithHeaderAndFooter()
{
QList<QString> header;
header << "1" << "2";
QList<QString> footer;
footer << "Here is a footer";
QList<QString> strList;
strList << "one" << "two" << "three";
QtCSV::StringData strData;
strData.addRow(strList);
const auto writeResult
= QtCSV::Writer::write(getFilePath(), strData, ",", {},
QtCSV::Writer::WriteMode::REWRITE, header, footer);
QVERIFY2(writeResult, "Failed to write to file");
const auto data = QtCSV::Reader::readToList(getFilePath());
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(3 == data.size(), "Wrong number of rows");
QVERIFY2(header == data.at(0), "Wrong header");
QVERIFY2(strList == data.at(1), "Wrong data");
QVERIFY2(footer == data.at(2), "Wrong footer");
}
void TestWriter::testWriterDataContainSeparators()
{
QList<QString> strList;
strList << "one" << "two" << "three, four";
QString strLine("this, is, one, element");
QtCSV::StringData strData;
strData.addRow(strList);
strData.addRow(strLine);
const auto writeResult
= QtCSV::Writer::write(getFilePath(), strData, ",", "\"");
QVERIFY2(writeResult, "Failed to write to file");
const auto data = QtCSV::Reader::readToList(getFilePath(), ",", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(2 == data.size(), "Wrong number of rows");
QVERIFY2(strList == data.at(0), "Wrong data at first row");
QVERIFY2((QList<QString>() << strLine) == data.at(1),
"Wrong data at second row");
}
void TestWriter::testWriteDifferentDataAmount()
{
auto rowsNumber = 10;
auto rowsMultiplier = 2;
auto rowCycles = 2;
QElapsedTimer time;
for (auto rc = 0; rc < rowCycles; ++rc)
{
auto symbolsNumber = 10;
auto symbolsMultiplier = 5;
auto symbolCycles = 4;
for (auto sc = 0; sc < symbolCycles; ++sc)
{
QtCSV::StringData data;
try
{
data = getTestStringData(symbolsNumber, rowsNumber);
}
catch (std::exception & /*e*/)
{
QFAIL("No enough memory to create data object");
}
QVERIFY2(!data.isEmpty(), "Failed to create content");
auto writeResult = false;
time.restart();
try
{
writeResult = QtCSV::Writer::write(getFilePath(), data, ",", {});
}
catch (std::exception & /*e*/)
{
QFAIL("No enough memory to write data to the file");
}
qDebug() << "symbols:" << symbolsNumber << ", rows:" << rowsNumber
<< ", time:" << time.elapsed() << "ms";
QVERIFY2(writeResult, "Failed to write to file");
QFile csvFile(getFilePath());
if (!csvFile.open(QIODevice::ReadOnly | QIODevice::Text))
{
QFAIL("Failed to open created csv-file");
}
QTextStream stream(&csvFile);
for (auto line = 0; line < data.rowCount(); ++line)
{
QList<QString> lineElements = stream.readLine().split(",");
QVERIFY2(data.rowValues(line) == lineElements,
"Original and result data are not the same");
}
csvFile.close();
symbolsNumber *= symbolsMultiplier;
}
rowsNumber *= rowsMultiplier;
}
}
QtCSV::StringData TestWriter::getTestStringData(qsizetype symbolsInRow,
qsizetype rowsNumber)
{
if (symbolsInRow <= 0 || rowsNumber <= 0)
{
qDebug() << __FUNCTION__ << "Invalid argumnets";
return {};
}
QList<QString> elements;
elements << "1234567890" << "3.14159265359" << "abcdefgh" << "ijklmnopqrs"
<< "tuvwxyz" << "ABCDEFGH" << "IJKLMNOPQRS" << "TUVWXYZ"
<< "some_STRANGE-string=" << "?!\\|/*+.<>@#$%^&(){}[]'`~";
QList<QString> rowElements;
auto rowLength = 0;
auto elementIndex = 0;
while (rowLength < symbolsInRow)
{
if (elements.size() <= elementIndex)
{
elementIndex = 0;
}
QString nextElement = elements.at(elementIndex);
if (symbolsInRow < rowLength + nextElement.size())
{
nextElement.resize(symbolsInRow - rowLength);
}
rowElements << nextElement;
rowLength += nextElement.size();
++elementIndex;
}
QtCSV::StringData data;
for (auto i = 0; i < rowsNumber; ++i)
{
data.addRow(rowElements);
}
return data;
}
void TestWriter::testWriteDataContainCRLF()
{
QList<QString> firstLine = QList<QString>()
<< "one" << "two" << "three\nfour,five";
QList<QString> secondLine = QList<QString>()
<< "six" << "seven,eight" << "nine,\rten";
QtCSV::StringData strData;
strData.addRow(firstLine);
strData.addRow(secondLine);
const auto writeResult
= QtCSV::Writer::write(getFilePath(), strData, ",", QString());
QVERIFY2(writeResult, "Failed to write to file");
const auto data = QtCSV::Reader::readToList(getFilePath(), ",", "\"");
QVERIFY2(!data.isEmpty(), "Failed to read file content");
QVERIFY2(2 == data.size(), "Wrong number of rows");
QVERIFY2(firstLine == data.at(0), "Wrong data at first row");
QVERIFY2(secondLine == data.at(1), "Wrong data at second row");
}

View File

@ -0,0 +1,39 @@
#ifndef TESTWRITER_H
#define TESTWRITER_H
#include <QObject>
#include <QtTest>
#include "qtcsv/stringdata.h"
class TestWriter : public QObject
{
Q_OBJECT
public:
TestWriter() = default;
private Q_SLOTS:
void cleanup();
void testWriteInvalidArgs();
void testWriteFromStringData();
void testWriteFromVariantData();
void testWriteToFileWithDotsInName();
void testWriteAppendMode();
void testWriteWithNotDefaultSeparator();
void testWriteWithHeader();
void testWriteWithFooter();
void testWriteWithHeaderAndFooter();
void testWriterDataContainSeparators();
void testWriteDifferentDataAmount();
void testWriteDataContainCRLF();
private:
QString getFilePath() const;
QString getFilePathXLS() const;
QString getFilePathWithDotsInName() const;
QtCSV::StringData getTestStringData(qsizetype symbolsInRow,
qsizetype rowsNumber);
};
#endif // TESTWRITER_H

View File

@ -0,0 +1,25 @@
#include <QtTest>
#include "testreader.h"
#include "teststringdata.h"
#include "testvariantdata.h"
#include "testwriter.h"
int AssertTest(QObject *obj)
{
int status = QTest::qExec(obj);
delete obj;
return status;
}
int main()
{
auto status = 0;
status |= AssertTest(new TestStringData());
status |= AssertTest(new TestVariantData());
status |= AssertTest(new TestReader());
status |= AssertTest(new TestWriter());
return status;
}