1 /* Reverse Engineer's Hex Editor
2  * Copyright (C) 2021 Daniel Collins <solemnwarning@solemnwarning.net>
3  *
4  * This program is free software; you can redistribute it and/or modify it
5  * under the terms of the GNU General Public License version 2 as published by
6  * the Free Software Foundation.
7  *
8  * This program is distributed in the hope that it will be useful, but WITHOUT
9  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
11  * more details.
12  *
13  * You should have received a copy of the GNU General Public License along with
14  * this program; if not, write to the Free Software Foundation, Inc., 51
15  * Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16 */
17 
18 #undef NDEBUG
19 #include "../src/platform.hpp"
20 #include <assert.h>
21 
22 #include <gtest/gtest.h>
23 #include <wx/evtloop.h>
24 #include <wx/frame.h>
25 
26 #include "../src/document.hpp"
27 #include "../src/search.hpp"
28 #include "../src/SharedDocumentPointer.hpp"
29 
30 using namespace REHex;
31 
32 class SearchBaseDummy: public Search
33 {
34 	public:
35 		bool should_wrap;
36 		bool wrap_requested;
37 		bool nothing_found;
38 
SearchBaseDummy(wxWindow * parent,SharedDocumentPointer & doc)39 		SearchBaseDummy(wxWindow *parent, SharedDocumentPointer &doc):
40 			Search(parent, doc, "Dummy search class"),
41 			should_wrap(false),
42 			wrap_requested(false),
43 			nothing_found(false) {}
44 
45 		/* NOTE: end_search() is called from subclass destructor rather than base to ensure search
46 		 * is stopped before the subclass becomes invalid, else there is a race where the base
47 		 * class will try calling the subclass's test() method and trigger undefined behaviour.
48 		*/
~SearchBaseDummy()49 		virtual ~SearchBaseDummy()
50 		{
51 			if(running)
52 			{
53 				end_search();
54 			}
55 		}
56 
test(const void * data,size_t data_size)57 		virtual bool test(const void *data, size_t data_size)
58 		{
59 			return (data_size >= 6 && memcmp(data, "foobar", 6) == 0)
60 				|| (data_size >= 3 && memcmp(data, "baz", 3) == 0);
61 		}
62 
test_max_window()63 		virtual size_t test_max_window() override
64 		{
65 			return 6;
66 		}
67 
set_range(off_t range_begin,off_t range_end)68 		void set_range(off_t range_begin, off_t range_end)
69 		{
70 			this->range_begin = range_begin;
71 			this->range_end   = range_end;
72 		}
73 
is_running()74 		bool is_running()
75 		{
76 			return running;
77 		}
78 
get_match()79 		off_t get_match()
80 		{
81 			return match_found_at;
82 		}
83 
84 	protected:
setup_window_controls(wxWindow * parent,wxSizer * sizer)85 		virtual void setup_window_controls(wxWindow *parent, wxSizer *sizer) override {}
read_window_controls()86 		virtual bool read_window_controls() override { return false; }
87 
wrap_query(const char * message)88 		virtual bool wrap_query(const char *message) override
89 		{
90 			wrap_requested = true;
91 			return should_wrap;
92 		}
93 
not_found_notification()94 		virtual void not_found_notification() override
95 		{
96 			nothing_found = true;
97 		}
98 };
99 
100 class SearchBaseTest: public ::testing::Test {
101 	protected:
102 		enum {
103 			ID_CHECK_TIMER = 1,
104 			ID_TIMEOUT_TIMER,
105 		};
106 
107 		wxFrame frame;
108 		SharedDocumentPointer doc;
109 		SearchBaseDummy s;
110 
111 		wxTimer *check_timer;
112 		wxTimer *timeout_timer;
113 
SearchBaseTest()114 		SearchBaseTest():
115 			frame(NULL, wxID_ANY, "REHex Tests"),
116 			doc(SharedDocumentPointer::make()),
117 			s(&frame, doc)
118 		{
119 			const std::vector<unsigned char> DATA(0x8192, 0x00);
120 			doc->insert_data(0, DATA.data(), DATA.size());
121 
122 			check_timer = new wxTimer(&frame, ID_CHECK_TIMER);
123 			timeout_timer = new wxTimer(&frame, ID_TIMEOUT_TIMER);
124 
125 			frame.Bind(wxEVT_TIMER, [this](wxTimerEvent &event)
126 			{
127 				if(!s.is_running())
128 				{
129 					wxTheApp->GetMainLoop()->ScheduleExit();
130 				}
131 			}, ID_CHECK_TIMER, ID_CHECK_TIMER);
132 
133 			frame.Bind(wxEVT_TIMER, [this](wxTimerEvent &event)
134 			{
135 				wxTheApp->GetMainLoop()->ScheduleExit();
136 			}, ID_TIMEOUT_TIMER, ID_TIMEOUT_TIMER);
137 		}
138 
wait_for_search()139 		void wait_for_search()
140 		{
141 			check_timer->Start(100, wxTIMER_CONTINUOUS);
142 			timeout_timer->Start(5000, wxTIMER_ONE_SHOT);
143 
144 			wxTheApp->OnRun();
145 
146 			timeout_timer->Stop();
147 			check_timer->Stop();
148 		}
149 
search_for_match(off_t sub_range_begin,off_t sub_range_end,Search::SearchDirection direction,off_t expect_match_at,off_t window_size=128)150 		void search_for_match(off_t sub_range_begin, off_t sub_range_end, Search::SearchDirection direction, off_t expect_match_at, off_t window_size = 128)
151 		{
152 			/* Starting a search will create a wxProgressDialog, which will install its
153 			 * own event loop(!) if one isn't already set up, which there isn't when
154 			 * running the tests, furthermore the dialog gets destroyed WITHIN the
155 			 * event loop that gets created by wxApp::OnRun() later, and the
156 			 * out-of-order event loop setup/destruction leads to a dangling event loop
157 			 * pointer! Yay!
158 			 *
159 			 * So we set up our own event loop just while the dialog is being created
160 			 * to avoid that.
161 			*/
162 
163 			{
164 				wxEventLoop loop;
165 				wxEventLoopActivator activate(&loop);
166 
167 				s.begin_search(sub_range_begin, sub_range_end, direction, window_size);
168 			}
169 
170 			wait_for_search();
171 
172 			EXPECT_FALSE(s.is_running());
173 			EXPECT_EQ(s.get_match(), expect_match_at);
174 
175 			if(s.should_wrap)
176 			{
177 				EXPECT_TRUE(s.wrap_requested);
178 			}
179 
180 			EXPECT_FALSE(s.nothing_found);
181 		}
182 
search_for_no_match(off_t sub_range_begin,off_t sub_range_end,Search::SearchDirection direction,off_t window_size=128)183 		void search_for_no_match(off_t sub_range_begin, off_t sub_range_end, Search::SearchDirection direction, off_t window_size = 128)
184 		{
185 			/* Starting a search will create a wxProgressDialog, which will install its
186 			 * own event loop(!) if one isn't already set up, which there isn't when
187 			 * running the tests, furthermore the dialog gets destroyed WITHIN the
188 			 * event loop that gets created by wxApp::OnRun() later, and the
189 			 * out-of-order event loop setup/destruction leads to a dangling event loop
190 			 * pointer! Yay!
191 			 *
192 			 * So we set up our own event loop just while the dialog is being created
193 			 * to avoid that.
194 			*/
195 
196 			{
197 				wxEventLoop loop;
198 				wxEventLoopActivator activate(&loop);
199 
200 				s.begin_search(sub_range_begin, sub_range_end, direction, window_size);
201 			}
202 
203 			wait_for_search();
204 
205 			EXPECT_FALSE(s.is_running());
206 			EXPECT_EQ(s.get_match(), -1);
207 
208 			if(s.should_wrap)
209 			{
210 				EXPECT_TRUE(s.wrap_requested);
211 				EXPECT_TRUE(s.nothing_found);
212 			}
213 			else{
214 				EXPECT_TRUE(s.wrap_requested ^ s.nothing_found);
215 			}
216 		}
217 };
218 
TEST_F(SearchBaseTest,ForwardsNoMatch)219 TEST_F(SearchBaseTest, ForwardsNoMatch)
220 {
221 	s.set_range(0, 8192);
222 
223 	search_for_no_match(0, 8192, Search::SearchDirection::FORWARDS);
224 }
225 
TEST_F(SearchBaseTest,BackwardsNoMatch)226 TEST_F(SearchBaseTest, BackwardsNoMatch)
227 {
228 	s.set_range(0, 8192);
229 
230 	search_for_no_match(0, 8192, Search::SearchDirection::BACKWARDS);
231 }
232 
TEST_F(SearchBaseTest,ForwardsMatch)233 TEST_F(SearchBaseTest, ForwardsMatch)
234 {
235 	doc->overwrite_data(1000, "foobar", 6);
236 	doc->overwrite_data(1500, "baz",    3);
237 
238 	s.set_range(0, 8192);
239 
240 	search_for_match(0, 8192, Search::SearchDirection::FORWARDS, 1000);
241 }
242 
TEST_F(SearchBaseTest,BackwardsMatch)243 TEST_F(SearchBaseTest, BackwardsMatch)
244 {
245 	doc->overwrite_data(1000, "foobar", 6);
246 	doc->overwrite_data(1500, "baz",    3);
247 
248 	s.set_range(0, 8192);
249 
250 	search_for_match(0, 8192, Search::SearchDirection::BACKWARDS, 1500);
251 }
252 
TEST_F(SearchBaseTest,ForwardsMatchBeforeRange)253 TEST_F(SearchBaseTest, ForwardsMatchBeforeRange)
254 {
255 	doc->overwrite_data(1000, "foobar", 6);
256 	doc->overwrite_data(1500, "baz",    3);
257 
258 	s.set_range(1501, 8192);
259 
260 	search_for_no_match(1501, 8192, Search::SearchDirection::FORWARDS);
261 }
262 
TEST_F(SearchBaseTest,ForwardsMatchAfterRange)263 TEST_F(SearchBaseTest, ForwardsMatchAfterRange)
264 {
265 	doc->overwrite_data(1000, "foobar", 6);
266 	doc->overwrite_data(1500, "baz",    3);
267 
268 	s.set_range(0, 1005);
269 
270 	search_for_no_match(0, 1005, Search::SearchDirection::FORWARDS);
271 }
272 
TEST_F(SearchBaseTest,BackwardsMatchBeforeRange)273 TEST_F(SearchBaseTest, BackwardsMatchBeforeRange)
274 {
275 	doc->overwrite_data(1000, "foobar", 6);
276 	doc->overwrite_data(1500, "baz",    3);
277 
278 	s.set_range(1501, 8192);
279 
280 	search_for_no_match(1501, 8192, Search::SearchDirection::BACKWARDS);
281 }
282 
TEST_F(SearchBaseTest,BackwardsMatchAfterRange)283 TEST_F(SearchBaseTest, BackwardsMatchAfterRange)
284 {
285 	doc->overwrite_data(1000, "foobar", 6);
286 	doc->overwrite_data(1500, "baz",    3);
287 
288 	s.set_range(0, 1005);
289 
290 	search_for_no_match(0, 1005, Search::SearchDirection::BACKWARDS);
291 }
292 
TEST_F(SearchBaseTest,ForwardsMatchBeforeSubRangeNoWrap)293 TEST_F(SearchBaseTest, ForwardsMatchBeforeSubRangeNoWrap)
294 {
295 	doc->overwrite_data(1000, "foobar", 6);
296 	doc->overwrite_data(1500, "baz",    3);
297 
298 	s.set_range(0, 8192);
299 
300 	search_for_no_match(1501, 8192, Search::SearchDirection::FORWARDS);
301 }
302 
TEST_F(SearchBaseTest,ForwardsMatchBeforeSubRangeWrap)303 TEST_F(SearchBaseTest, ForwardsMatchBeforeSubRangeWrap)
304 {
305 	doc->overwrite_data(1000, "foobar", 6);
306 	doc->overwrite_data(1500, "baz",    3);
307 
308 	s.set_range(0, 8192);
309 	s.should_wrap = true;
310 
311 	search_for_match(1501, 8192, Search::SearchDirection::FORWARDS, 1000);
312 }
313 
TEST_F(SearchBaseTest,ForwardsMatchAtStartOfRangeBeforeSubRangeWrap)314 TEST_F(SearchBaseTest, ForwardsMatchAtStartOfRangeBeforeSubRangeWrap)
315 {
316 	doc->overwrite_data(1000, "foobar", 6);
317 	doc->overwrite_data(1500, "baz",    3);
318 
319 	s.set_range(1000, 8192);
320 	s.should_wrap = true;
321 
322 	search_for_match(1600, 8192, Search::SearchDirection::FORWARDS, 1000);
323 }
324 
TEST_F(SearchBaseTest,ForwardsMatchStraddlingEndOfSubRangeBeforeSubRangeWrap)325 TEST_F(SearchBaseTest, ForwardsMatchStraddlingEndOfSubRangeBeforeSubRangeWrap)
326 {
327 	doc->overwrite_data(1500, "baz", 3);
328 
329 	s.set_range(0, 8192);
330 	s.should_wrap = true;
331 
332 	search_for_match(1502, 8192, Search::SearchDirection::FORWARDS, 1500);
333 }
334 
TEST_F(SearchBaseTest,BackwardsMatchAfterSubRangeNoWrap)335 TEST_F(SearchBaseTest, BackwardsMatchAfterSubRangeNoWrap)
336 {
337 	doc->overwrite_data(1000, "foobar", 6);
338 	doc->overwrite_data(1500, "baz",    3);
339 
340 	s.set_range(0, 8192);
341 
342 	search_for_no_match(0, 1005, Search::SearchDirection::BACKWARDS);
343 }
344 
TEST_F(SearchBaseTest,BackwardsMatchAfterSubRangeWrap)345 TEST_F(SearchBaseTest, BackwardsMatchAfterSubRangeWrap)
346 {
347 	doc->overwrite_data(1000, "foobar", 6);
348 	doc->overwrite_data(1500, "baz",    3);
349 
350 	s.set_range(0, 8192);
351 	s.should_wrap = true;
352 
353 	search_for_match(0, 1005, Search::SearchDirection::BACKWARDS, 1500);
354 }
355 
TEST_F(SearchBaseTest,BackwardsMatchAtEndOfRangeAfterSubRangeWrap)356 TEST_F(SearchBaseTest, BackwardsMatchAtEndOfRangeAfterSubRangeWrap)
357 {
358 	doc->overwrite_data(1500, "baz", 3);
359 
360 	s.set_range(0, 1503);
361 	s.should_wrap = true;
362 
363 	search_for_match(0, 1005, Search::SearchDirection::BACKWARDS, 1500);
364 }
365 
TEST_F(SearchBaseTest,BackwardsMatchStraddlingStartOfSubRangeAfterSubRangeWrap)366 TEST_F(SearchBaseTest, BackwardsMatchStraddlingStartOfSubRangeAfterSubRangeWrap)
367 {
368 	doc->overwrite_data(1000, "foobar", 6);
369 
370 	s.set_range(0, 8192);
371 	s.should_wrap = true;
372 
373 	search_for_match(0, 1002, Search::SearchDirection::BACKWARDS, 1000);
374 }
375 
TEST_F(SearchBaseTest,ForwardsMatchStartingBeforeRange)376 TEST_F(SearchBaseTest, ForwardsMatchStartingBeforeRange)
377 {
378 	doc->overwrite_data(1000, "foobar", 6);
379 	doc->overwrite_data(1500, "baz",    3);
380 
381 	s.set_range(1000, 8192);
382 
383 	search_for_match(0, 8192, Search::SearchDirection::FORWARDS, 1000);
384 }
385 
TEST_F(SearchBaseTest,ForwardsMatchStartingAfterRange)386 TEST_F(SearchBaseTest, ForwardsMatchStartingAfterRange)
387 {
388 	doc->overwrite_data(1000, "foobar", 6);
389 	doc->overwrite_data(1500, "baz",    3);
390 
391 	s.set_range(1000, 4096);
392 	s.should_wrap = true;
393 
394 	search_for_match(6000, 4096, Search::SearchDirection::FORWARDS, 1000);
395 }
396 
TEST_F(SearchBaseTest,BackwardsMatchToBeforeRange)397 TEST_F(SearchBaseTest, BackwardsMatchToBeforeRange)
398 {
399 	doc->overwrite_data(1000, "foobar", 6);
400 	doc->overwrite_data(1500, "baz",    3);
401 
402 	s.set_range(1000, 4096);
403 	s.should_wrap = true;
404 
405 	search_for_match(1000, 800, Search::SearchDirection::BACKWARDS, 1500);
406 }
407 
TEST_F(SearchBaseTest,BackwardsMatchToAfterRange)408 TEST_F(SearchBaseTest, BackwardsMatchToAfterRange)
409 {
410 	doc->overwrite_data(1000, "foobar", 6);
411 	doc->overwrite_data(1500, "baz",    3);
412 
413 	s.set_range(1000, 4096);
414 
415 	search_for_match(1000, 8000, Search::SearchDirection::BACKWARDS, 1500);
416 }
417 
TEST_F(SearchBaseTest,ForwardsZeroLengthRange)418 TEST_F(SearchBaseTest, ForwardsZeroLengthRange)
419 {
420 	doc->overwrite_data(1000, "foobar", 6);
421 	doc->overwrite_data(1500, "baz",    3);
422 
423 	s.set_range(1000, 1000);
424 
425 	search_for_no_match(1000, 1000, Search::SearchDirection::FORWARDS);
426 }
427 
TEST_F(SearchBaseTest,BackwardsZeroLengthRange)428 TEST_F(SearchBaseTest, BackwardsZeroLengthRange)
429 {
430 	doc->overwrite_data(1000, "foobar", 6);
431 	doc->overwrite_data(1500, "baz",    3);
432 
433 	s.set_range(1000, 1000);
434 
435 	search_for_no_match(1000, 1000, Search::SearchDirection::BACKWARDS);
436 }
437