File: error_in_creator.cpp

package info (click to toggle)
zimlib 8.1.1-0.2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,592 kB
  • sloc: cpp: 14,011; ansic: 548; python: 241; makefile: 8; sh: 5
file content (387 lines) | stat: -rw-r--r-- 13,790 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
/*
 * Copyright (C) 2022 Matthieu Gautier <mgautier@kymeria.fr>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
 * NON-INFRINGEMENT.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 *
 */

#include <zim/zim.h>
#include <zim/writer/creator.h>
#include <zim/writer/item.h>
#include <zim/writer/contentProvider.h>

#include "tools.h"
#include "../src/tools.h"
#include <zim/error.h>
#include "gtest/gtest.h"
#include "config.h"

namespace
{

using namespace zim;

enum class ERRORKIND {
  PATH,                                  // 0
  TITLE,                                 // 1
  MIMETYPE,                              // 2
  HINTS,                                 // 3
  GET_CONTENTPROVIDER,                   // 4
  EXCEPTION_CONTENTPROVIDER_SIZE,        // 5
  EXCEPTION_CONTENTPROVIDER_FEED,        // 6
  WRONG_OVER_SIZE_CONTENTPROVIDER,       // 7
  WRONG_UNDER_SIZE_CONTENTPROVIDER,      // 8
  GET_INDEXDATA,                         // 9
  HAS_INDEXDATA,                         // 10
  GET_INDEXDATA_TITLE,                   // 11
  GET_INDEXDATA_CONTENT,                 // 12
  GET_INDEXDATA_KEYWORD,                 // 13
  GET_INDEXDATA_WORDCOUNT,               // 14
  GET_INDEXDATA_POSITION                 // 15
};

class SimulatedFaultError : public std::runtime_error
{
  public:
    SimulatedFaultError(const std::string& err) : std::runtime_error(err) {}
};

#define THROW_ERROR(KIND) if (ERRORKIND::KIND == fault) { throw SimulatedFaultError(#KIND); } else {}

class FaultyContentProvider : public writer::StringProvider
{
  public:
    FaultyContentProvider(const std::string& content, ERRORKIND fault)
      : writer::StringProvider(content),
        fault(fault)
    {}

  zim::size_type getSize() const override {
    THROW_ERROR(EXCEPTION_CONTENTPROVIDER_SIZE)
    auto size = writer::StringProvider::getSize();
    if (fault == ERRORKIND::WRONG_OVER_SIZE_CONTENTPROVIDER) {
      return size + 1;
    }
    if (fault == ERRORKIND::WRONG_UNDER_SIZE_CONTENTPROVIDER) {
      return size - 1;
    }
    return size;
  }

  Blob feed() override {
    THROW_ERROR(EXCEPTION_CONTENTPROVIDER_FEED)
    return writer::StringProvider::feed();
  }

  private:
    ERRORKIND fault;
};


class FaultyIndexData : public writer::IndexData
{
  public:
    explicit FaultyIndexData(ERRORKIND fault)
     : fault(fault)
    {}

    bool hasIndexData() const override {
      THROW_ERROR(HAS_INDEXDATA)
      return true;
    }

    std::string getTitle() const override {
      THROW_ERROR(GET_INDEXDATA_TITLE)
      return "Foo";
    }

    std::string getContent() const override {
      THROW_ERROR(GET_INDEXDATA_CONTENT)
      return "FooContent";
    }

    std::string getKeywords() const override {
      THROW_ERROR(GET_INDEXDATA_KEYWORD)
      return "";
    }

    uint32_t getWordCount() const override {
      THROW_ERROR(GET_INDEXDATA_WORDCOUNT)
      return 1;
    }

    GeoPosition getGeoPosition() const override {
      THROW_ERROR(GET_INDEXDATA_POSITION)
      return GeoPosition();
    }

  private:
    ERRORKIND fault;
};


class FaultyItem : public writer::Item
{
  public:
    FaultyItem(const std::string& path, const std::string& title, const std::string& content, bool compressed, ERRORKIND fault):
     path(path), title(title), content(content), compressed(compressed), fault(fault)  { }
    virtual ~FaultyItem() = default;

    std::string getPath() const override { THROW_ERROR(PATH) return path; };
    std::string getTitle() const override { THROW_ERROR(TITLE) return title; };
    std::string getMimeType() const override { THROW_ERROR(MIMETYPE) return "text/html"; };
    writer::Hints getHints() const override {
      THROW_ERROR(HINTS)
      return { { writer::COMPRESS, compressed } };
    }

    std::unique_ptr<writer::ContentProvider> getContentProvider() const override {
      THROW_ERROR(GET_CONTENTPROVIDER)
      return std::unique_ptr<writer::ContentProvider>(new FaultyContentProvider(content, fault));
    }

    std::shared_ptr<writer::IndexData> getIndexData() const override {
      THROW_ERROR(GET_INDEXDATA)
      return std::shared_ptr<writer::IndexData>(new FaultyIndexData(fault));
    }

  std::string path;
  std::string title;
  std::string content;
  bool compressed;
  ERRORKIND fault;
};


class FaultyItemErrorTest : public ::testing::TestWithParam<ERRORKIND> {};

TEST_P(FaultyItemErrorTest, faultyItem)
{
  unittests::TempFile temp("zimfile");
  auto tempPath = temp.path();

  writer::Creator creator;
  creator.configIndexing(true, "eng");
  creator.startZimCreation(tempPath);
  auto item = std::make_shared<FaultyItem>("foo", "Foo", "FooContent", true, GetParam());
  // A error in the item is directly reported
  EXPECT_THROW(creator.addItem(item), SimulatedFaultError);
  // As the error is directly reported, finishZimCreation report nothing.
  EXPECT_NO_THROW(creator.finishZimCreation());
}

// It would be more natural to put the `#if defined` only around the
// discarded values, but when crosscompiling on Windows, compiler fail to
// understand ``#if defined` when used inside the `INSTANTIATE_TEST_CASE_P`
// macro. I suspect some macro definition conflicts.
#if defined(ENABLE_XAPIAN)
INSTANTIATE_TEST_CASE_P(
CreatorError,
FaultyItemErrorTest,
::testing::Values(
    ERRORKIND::PATH,
    ERRORKIND::TITLE,
    ERRORKIND::MIMETYPE,
    ERRORKIND::HINTS,
    ERRORKIND::GET_CONTENTPROVIDER,
    ERRORKIND::EXCEPTION_CONTENTPROVIDER_SIZE,
    ERRORKIND::GET_INDEXDATA
));
#else
INSTANTIATE_TEST_CASE_P(
CreatorError,
FaultyItemErrorTest,
::testing::Values(
    ERRORKIND::PATH,
    ERRORKIND::TITLE,
    ERRORKIND::MIMETYPE,
    ERRORKIND::HINTS,
    ERRORKIND::GET_CONTENTPROVIDER,
    ERRORKIND::EXCEPTION_CONTENTPROVIDER_SIZE
));
#endif //ENABLE_XAPIAN

double getWaitTimeFactor() {
  char* str_time_factor = std::getenv("WAIT_TIME_FACTOR_TEST");
  if (str_time_factor) {
    // Yes, if user set a "non float" value, sleep_time will be 0. But not our problem here
    // Same thing if user set a negative value.
    return atof(str_time_factor);
  }
  return 1.0;
}


class FaultyDelayedItemErrorTest : public ::testing::TestWithParam<ERRORKIND> {};


// All the following code "should" thrown a error :
// - An asyncError on the first call (async_error_thrown == false)
// - An CreatorStateError on other calls (async_error_thrown == true)
// But it may not thrown if worker thread has not run.
// (and only in this case. If a AsyncError has been thrown, other calls MUST throw a CreatorErrorState).
// FinishZimCreation() always waits for workers, so we MUST have an exception.
#define CHECK_ASYNC_EXCEPT(CALL) \
  { \
    const char* something_went_wrong = nullptr; \
    const bool MUST_FAIL = std::string(#CALL).find("finishZimCreation") != std::string::npos; \
    try { \
      CALL; \
      if (async_error_thrown) { \
        something_went_wrong = "We should have thrown a CreatorStateError after AsyncError has been detected."; \
      } \
      if (MUST_FAIL) { \
        something_went_wrong = "The call should have thrown an exception."; \
      } \
    } catch (AsyncError& e) { \
      if (async_error_thrown) { \
        something_went_wrong = "We should have thrown a CreatorStateError after AsyncError has been detected."; \
      } \
      async_error_thrown = true; \
    } catch (CreatorStateError& e) { \
      if (!async_error_thrown) { \
        something_went_wrong = "CreatorStateError must be thrown after a AsyncError."; \
      } \
    } catch (...) { \
      something_went_wrong = "An exception other than CreatorStateError or AsyncError was thrown."; \
    } \
    if (something_went_wrong) { \
      FAIL() << something_went_wrong; \
    } \
  }


// Compressed and uncompressed content use a different code path as
// compressed cluster uses contentProvider when the cluster is closed (compressed)
// and uncompressed cluster uses contentProvider when the cluster is written.
TEST_P(FaultyDelayedItemErrorTest, faultyCompressedItem)
{
  unittests::TempFile temp("zimfile");
  auto tempPath = temp.path();

  bool async_error_thrown = false;
  writer::Creator creator;
  creator.configIndexing(true, "eng");
  creator.configClusterSize(5);
  creator.startZimCreation(tempPath);
  auto item = std::make_shared<FaultyItem>("foo", "Foo", "FooContent", true, GetParam());
  // Exception is not thrown in main thread so error is not detected
  EXPECT_NO_THROW(creator.addItem(item));
  // We force the closing of the cluster, so working thread will detect error
  CHECK_ASYNC_EXCEPT(creator.addMetadata("A metadata", "A compressed (default) metadata"));
  // give a chance to threads to detect the error.
  // How many time to wait is a bit tricky.
  // Too long and all tests will wait to much and developpers hate to wait,
  // Not enough and error is not deteced and tests fail (and developpers hate failing tets)
  // The exact value is specific to each computer, so we need to make this configurable.
  // We use a base and we multiply it by a factor taken from env variable.
  const long sleep_time = 1000000; // Default value is set to a factor 10 above what is needed to work on my (fast) computer
  zim::microsleep(sleep_time * getWaitTimeFactor());
  // We detect it for any call after
  CHECK_ASYNC_EXCEPT(creator.addMetadata("Title", "This is a title"));
  CHECK_ASYNC_EXCEPT(creator.addMetadata("Title", "This is a title"));
  CHECK_ASYNC_EXCEPT(creator.addIllustration(48, "PNGBinaryContent48"));
  CHECK_ASYNC_EXCEPT(creator.addRedirection("foo2", "FooRedirection", "foo"));
  CHECK_ASYNC_EXCEPT(creator.finishZimCreation());
}

TEST_P(FaultyDelayedItemErrorTest, faultyUnCompressedItem)
{
  unittests::TempFile temp("zimfile");
  auto tempPath = temp.path();

  bool async_error_thrown = false;
  writer::Creator creator;
  creator.configIndexing(true, "eng");
  creator.configClusterSize(5);
  creator.startZimCreation(tempPath);
  auto item = std::make_shared<FaultyItem>("foo", "Foo", "FooContent", false, GetParam());
  // Exception is not thrown in main thread so error is not detected
  EXPECT_NO_THROW(creator.addItem(item));
  // We force the closing of the cluster, so working thread will detect error
  CHECK_ASYNC_EXCEPT(creator.addMetadata("A metadata", "A uncompressed metadata", "plain/content"));
  // give a chance to threads to detect the error
  // How many time to wait is a bit tricky.
  // Too long and all tests will wait to much and developpers hate to wait
  // Not enough and error is not deteced and tests fail (and developpers hate failing tets)
  // The exacte value is specific to each computer, so we need to make this configurable.
  // We use a base and we multiply it by a factor taken from env variable.
  // Note here, that we have a base smaller than for compressed test as we don't compress the content
  // and the writer thread (the one using the contentProvider) detect the error sooner
  const long sleep_time = 10000; // Default value is set to a factor 10 above what is needed to work on my (fast) computer
  zim::microsleep(sleep_time * getWaitTimeFactor());
  // But we detect it for any call after
  CHECK_ASYNC_EXCEPT(creator.addMetadata("Title", "This is a title"));
  CHECK_ASYNC_EXCEPT(creator.addIllustration(48, "PNGBinaryContent48"));
  CHECK_ASYNC_EXCEPT(creator.addRedirection("foo2", "FooRedirection", "foo"));
  CHECK_ASYNC_EXCEPT(creator.finishZimCreation());
}


// Check that destructor correctly clean everything on error
// even if finishZimCreation is not called.
TEST_P(FaultyDelayedItemErrorTest, faultyUnfinishedCreator)
{
  unittests::TempFile tmpFile("zimfile");
  {
    writer::Creator creator;
    creator.configIndexing(true, "eng");
    creator.configClusterSize(5);
    creator.startZimCreation(tmpFile.path());
    auto item = std::make_shared<FaultyItem>("foo", "Foo", "FooContent", true, GetParam());
    // Exception is not thrown in main thread so error is not detected
    EXPECT_NO_THROW(creator.addItem(item));
    // creator.finishZimCreation() is not called
  }

  EXPECT_THROW(
      {
        const zim::Archive archive(tmpFile.path());
      },
      zim::ZimFileFormatError
  );
}
// It would be more natural to put the `#if defined` only around the
// discarded values, but when crosscompiling on Windows, compiler fail to
// understand ``#if defined` when used inside the `INSTANTIATE_TEST_CASE_P`
// macro. I suspect some macro definition conflicts.
#if defined(ENABLE_XAPIAN)
INSTANTIATE_TEST_CASE_P(
CreatorError,
FaultyDelayedItemErrorTest,
::testing::Values(
    ERRORKIND::EXCEPTION_CONTENTPROVIDER_FEED,
    ERRORKIND::WRONG_OVER_SIZE_CONTENTPROVIDER,
    ERRORKIND::WRONG_UNDER_SIZE_CONTENTPROVIDER ,
    ERRORKIND::HAS_INDEXDATA,
    ERRORKIND::GET_INDEXDATA_TITLE,
    ERRORKIND::GET_INDEXDATA_CONTENT,
    ERRORKIND::GET_INDEXDATA_KEYWORD,
    ERRORKIND::GET_INDEXDATA_WORDCOUNT,
    ERRORKIND::GET_INDEXDATA_POSITION
));
#else
INSTANTIATE_TEST_CASE_P(
CreatorError,
FaultyDelayedItemErrorTest,
::testing::Values(
    ERRORKIND::EXCEPTION_CONTENTPROVIDER_FEED,
    ERRORKIND::WRONG_OVER_SIZE_CONTENTPROVIDER,
    ERRORKIND::WRONG_UNDER_SIZE_CONTENTPROVIDER
));
#endif // ENABLE_XAPIAN
} // unnamed namespace