testoscope 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 617847638870b0e1393458cc325f0f2ec9f0ec00
4
+ data.tar.gz: 400016aae971293b20d7241fd8618ef560d25087
5
+ SHA512:
6
+ metadata.gz: 57f2654d2b92a7e7c8e8e6edb2f5f8be748cb0aac1bb006867b6547284b8ca09b40f1185f8f942029e86cc822e11c9667441cae2026250202a57a8e612a0a5cc
7
+ data.tar.gz: d72ad93aaf2f07f3e3da160a0177038e16a6f1cd8b314025f20f33b5c1f362ae6bda8e2c59799ef243acdb2cd1b7a33604648cb8c175571efd82900c2cec1338
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.idea/
2
+ /.bundle/
3
+ /.yardoc
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.idea/misc.xml ADDED
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="ruby-2.0.0-p648" project-jdk-type="RUBY_SDK" />
4
+ </project>
data/.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/testoscope.iml" filepath="$PROJECT_DIR$/.idea/testoscope.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="ModuleRunConfigurationManager">
4
+ <shared />
5
+ </component>
6
+ <component name="NewModuleRootManager">
7
+ <content url="file://$MODULE_DIR$" />
8
+ <orderEntry type="jdk" jdkName="RVM: ruby-2.4.1 [apivore]" jdkType="RUBY_SDK" />
9
+ <orderEntry type="sourceFolder" forTests="false" />
10
+ </component>
11
+ </module>
@@ -0,0 +1,269 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ChangeListManager">
4
+ <list default="true" id="2c89485b-c647-4006-8e76-d4552e0de6c3" name="Default" comment="">
5
+ <change beforePath="" afterPath="$PROJECT_DIR$/.idea/misc.xml" />
6
+ <change beforePath="" afterPath="$PROJECT_DIR$/.idea/modules.xml" />
7
+ <change beforePath="" afterPath="$PROJECT_DIR$/.idea/testoscope.iml" />
8
+ <change beforePath="" afterPath="$PROJECT_DIR$/.idea/workspace.xml" />
9
+ <change beforePath="" afterPath="$PROJECT_DIR$/.travis.yml" />
10
+ <change beforePath="$PROJECT_DIR$/README.md" afterPath="$PROJECT_DIR$/README.md" />
11
+ </list>
12
+ <option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
13
+ <option name="TRACKING_ENABLED" value="true" />
14
+ <option name="SHOW_DIALOG" value="false" />
15
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
16
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
17
+ <option name="LAST_RESOLUTION" value="IGNORE" />
18
+ </component>
19
+ <component name="FileEditorManager">
20
+ <leaf>
21
+ <file leaf-file-name="testoscope.gemspec" pinned="false" current-in-tab="false">
22
+ <entry file="file://$PROJECT_DIR$/testoscope.gemspec">
23
+ <provider selected="true" editor-type-id="text-editor">
24
+ <state relative-caret-position="403">
25
+ <caret line="39" column="42" lean-forward="false" selection-start-line="39" selection-start-column="42" selection-end-line="39" selection-end-column="42" />
26
+ <folding />
27
+ </state>
28
+ </provider>
29
+ </entry>
30
+ </file>
31
+ <file leaf-file-name="testoscope.rb" pinned="false" current-in-tab="false">
32
+ <entry file="file://$PROJECT_DIR$/lib/testoscope.rb">
33
+ <provider selected="true" editor-type-id="text-editor">
34
+ <state relative-caret-position="120">
35
+ <caret line="8" column="0" lean-forward="false" selection-start-line="8" selection-start-column="0" selection-end-line="13" selection-end-column="24" />
36
+ <folding />
37
+ </state>
38
+ </provider>
39
+ </entry>
40
+ </file>
41
+ <file leaf-file-name="README.md" pinned="false" current-in-tab="true">
42
+ <entry file="file://$PROJECT_DIR$/README.md">
43
+ <provider selected="true" editor-type-id="split-provider[text-editor;markdown-preview-editor]">
44
+ <state split_layout="SPLIT">
45
+ <first_editor relative-caret-position="327">
46
+ <caret line="121" column="0" lean-forward="true" selection-start-line="121" selection-start-column="0" selection-end-line="121" selection-end-column="0" />
47
+ <folding />
48
+ </first_editor>
49
+ <second_editor />
50
+ </state>
51
+ </provider>
52
+ </entry>
53
+ </file>
54
+ <file leaf-file-name=".gitignore" pinned="false" current-in-tab="false">
55
+ <entry file="file://$PROJECT_DIR$/.gitignore">
56
+ <provider selected="true" editor-type-id="text-editor">
57
+ <state relative-caret-position="0">
58
+ <caret line="0" column="7" lean-forward="false" selection-start-line="0" selection-start-column="7" selection-end-line="0" selection-end-column="7" />
59
+ <folding />
60
+ </state>
61
+ </provider>
62
+ </entry>
63
+ </file>
64
+ </leaf>
65
+ </component>
66
+ <component name="Git.Settings">
67
+ <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
68
+ </component>
69
+ <component name="IdeDocumentHistory">
70
+ <option name="CHANGED_PATHS">
71
+ <list>
72
+ <option value="$PROJECT_DIR$/lib/testoscope.rb" />
73
+ <option value="$PROJECT_DIR$/.gitignore" />
74
+ <option value="$PROJECT_DIR$/testoscope.gemspec" />
75
+ <option value="$PROJECT_DIR$/README.md" />
76
+ </list>
77
+ </option>
78
+ </component>
79
+ <component name="JsBuildToolGruntFileManager" detection-done="true" sorting="DEFINITION_ORDER" />
80
+ <component name="JsBuildToolPackageJson" detection-done="true" sorting="DEFINITION_ORDER" />
81
+ <component name="JsGulpfileManager">
82
+ <detection-done>true</detection-done>
83
+ <sorting>DEFINITION_ORDER</sorting>
84
+ </component>
85
+ <component name="ProjectFrameBounds" fullScreen="true">
86
+ <option name="width" value="1440" />
87
+ <option name="height" value="900" />
88
+ </component>
89
+ <component name="ProjectLevelVcsManager" settingsEditedManually="true" />
90
+ <component name="ProjectView">
91
+ <navigator currentView="ProjectPane" proportions="" version="1">
92
+ <flattenPackages />
93
+ <showMembers />
94
+ <showModules />
95
+ <showLibraryContents />
96
+ <hideEmptyPackages />
97
+ <abbreviatePackageNames />
98
+ <autoscrollToSource />
99
+ <autoscrollFromSource />
100
+ <sortByType />
101
+ <manualOrder />
102
+ <foldersAlwaysOnTop value="true" />
103
+ </navigator>
104
+ <panes>
105
+ <pane id="Scope" />
106
+ <pane id="Scratches" />
107
+ <pane id="ProjectPane">
108
+ <subPane>
109
+ <expand>
110
+ <path>
111
+ <item name="testoscope" type="b2602c69:ProjectViewProjectNode" />
112
+ <item name="testoscope" type="462c0819:PsiDirectoryNode" />
113
+ </path>
114
+ <path>
115
+ <item name="testoscope" type="b2602c69:ProjectViewProjectNode" />
116
+ <item name="testoscope" type="462c0819:PsiDirectoryNode" />
117
+ <item name="lib" type="462c0819:PsiDirectoryNode" />
118
+ </path>
119
+ </expand>
120
+ <select />
121
+ </subPane>
122
+ </pane>
123
+ </panes>
124
+ </component>
125
+ <component name="PropertiesComponent">
126
+ <property name="settings.editor.selected.configurable" value="org.jetbrains.plugins.ruby.settings.RubyActiveModuleSdkConfigurable" />
127
+ <property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
128
+ <property name="WebServerToolWindowFactoryState" value="false" />
129
+ </component>
130
+ <component name="RecentsManager">
131
+ <key name="CopyFile.RECENT_KEYS">
132
+ <recent name="$PROJECT_DIR$" />
133
+ </key>
134
+ </component>
135
+ <component name="RunDashboard">
136
+ <option name="ruleStates">
137
+ <list>
138
+ <RuleState>
139
+ <option name="name" value="ConfigurationTypeDashboardGroupingRule" />
140
+ </RuleState>
141
+ <RuleState>
142
+ <option name="name" value="StatusDashboardGroupingRule" />
143
+ </RuleState>
144
+ </list>
145
+ </option>
146
+ </component>
147
+ <component name="ShelveChangesManager" show_recycled="false">
148
+ <option name="remove_strategy" value="false" />
149
+ </component>
150
+ <component name="SpringUtil" SPRING_PRE_LOADER_OPTION="true" />
151
+ <component name="SvnConfiguration">
152
+ <configuration />
153
+ </component>
154
+ <component name="TaskManager">
155
+ <task active="true" id="Default" summary="Default task">
156
+ <changelist id="2c89485b-c647-4006-8e76-d4552e0de6c3" name="Default" comment="" />
157
+ <created>1521465490978</created>
158
+ <option name="number" value="Default" />
159
+ <option name="presentableId" value="Default" />
160
+ <updated>1521465490978</updated>
161
+ <workItem from="1521465492548" duration="13819000" />
162
+ </task>
163
+ <task id="LOCAL-00001" summary="initial commit">
164
+ <created>1521545104500</created>
165
+ <option name="number" value="00001" />
166
+ <option name="presentableId" value="LOCAL-00001" />
167
+ <option name="project" value="LOCAL" />
168
+ <updated>1521545104500</updated>
169
+ </task>
170
+ <task id="LOCAL-00002" summary="read me chngs">
171
+ <created>1521545270044</created>
172
+ <option name="number" value="00002" />
173
+ <option name="presentableId" value="LOCAL-00002" />
174
+ <option name="project" value="LOCAL" />
175
+ <updated>1521545270044</updated>
176
+ </task>
177
+ <task id="LOCAL-00003" summary="more bold">
178
+ <created>1521545367711</created>
179
+ <option name="number" value="00003" />
180
+ <option name="presentableId" value="LOCAL-00003" />
181
+ <option name="project" value="LOCAL" />
182
+ <updated>1521545367711</updated>
183
+ </task>
184
+ <option name="localTasksCounter" value="4" />
185
+ <servers />
186
+ </component>
187
+ <component name="TimeTrackingManager">
188
+ <option name="totallyTimeSpent" value="13819000" />
189
+ </component>
190
+ <component name="ToolWindowManager">
191
+ <frame x="0" y="0" width="1440" height="900" extended-state="0" />
192
+ <editor active="true" />
193
+ <layout>
194
+ <window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.14663805" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
195
+ <window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
196
+ <window_info id="Docker" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="false" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
197
+ <window_info id="Event Log" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="-1" side_tool="true" content_ui="tabs" />
198
+ <window_info id="Database" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
199
+ <window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.32889965" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
200
+ <window_info id="Version Control" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
201
+ <window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
202
+ <window_info id="Terminal" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
203
+ <window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
204
+ <window_info id="Favorites" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="-1" side_tool="true" content_ui="tabs" />
205
+ <window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
206
+ <window_info id="Hierarchy" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="combo" />
207
+ <window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
208
+ <window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
209
+ <window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
210
+ <window_info id="Inspection" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
211
+ <window_info id="Ant Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
212
+ </layout>
213
+ </component>
214
+ <component name="TypeScriptGeneratedFilesManager">
215
+ <option name="version" value="1" />
216
+ </component>
217
+ <component name="VcsContentAnnotationSettings">
218
+ <option name="myLimit" value="2678400000" />
219
+ </component>
220
+ <component name="VcsManagerConfiguration">
221
+ <MESSAGE value="initial commit" />
222
+ <MESSAGE value="read me chngs" />
223
+ <MESSAGE value="more bold" />
224
+ <option name="LAST_COMMIT_MESSAGE" value="more bold" />
225
+ </component>
226
+ <component name="XDebuggerManager">
227
+ <breakpoint-manager>
228
+ <option name="time" value="1" />
229
+ </breakpoint-manager>
230
+ <watches-manager />
231
+ </component>
232
+ <component name="editorHistoryManager">
233
+ <entry file="file://$PROJECT_DIR$/lib/testoscope.rb">
234
+ <provider selected="true" editor-type-id="text-editor">
235
+ <state relative-caret-position="120">
236
+ <caret line="8" column="0" lean-forward="false" selection-start-line="8" selection-start-column="0" selection-end-line="13" selection-end-column="24" />
237
+ <folding />
238
+ </state>
239
+ </provider>
240
+ </entry>
241
+ <entry file="file://$PROJECT_DIR$/.gitignore">
242
+ <provider selected="true" editor-type-id="text-editor">
243
+ <state relative-caret-position="0">
244
+ <caret line="0" column="7" lean-forward="false" selection-start-line="0" selection-start-column="7" selection-end-line="0" selection-end-column="7" />
245
+ <folding />
246
+ </state>
247
+ </provider>
248
+ </entry>
249
+ <entry file="file://$PROJECT_DIR$/testoscope.gemspec">
250
+ <provider selected="true" editor-type-id="text-editor">
251
+ <state relative-caret-position="403">
252
+ <caret line="39" column="42" lean-forward="false" selection-start-line="39" selection-start-column="42" selection-end-line="39" selection-end-column="42" />
253
+ <folding />
254
+ </state>
255
+ </provider>
256
+ </entry>
257
+ <entry file="file://$PROJECT_DIR$/README.md">
258
+ <provider selected="true" editor-type-id="split-provider[text-editor;markdown-preview-editor]">
259
+ <state split_layout="SPLIT">
260
+ <first_editor relative-caret-position="327">
261
+ <caret line="121" column="0" lean-forward="true" selection-start-line="121" selection-start-column="0" selection-end-line="121" selection-end-column="0" />
262
+ <folding />
263
+ </first_editor>
264
+ <second_editor />
265
+ </state>
266
+ </provider>
267
+ </entry>
268
+ </component>
269
+ </project>
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in testoscope.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 alekseyl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # Testoscope
2
+
3
+ Inspect in practice how well is your data organized while testing your application!
4
+
5
+ ## Features
6
+ **Finds out of the box:** sequential scans, dummy One-Timer calls to DB, unused indexes.
7
+
8
+ **Highly customizable:** you can define your own unintended markers, inspect only part of you tables set and so.
9
+
10
+ May work in a **error mode** raising exception on unintended behaviour,
11
+ that way you can protect from perfomance break-out in production.
12
+
13
+ Best suits with high-level testing: **controller tests, integration tests, api tests** and so.
14
+
15
+ Output example:
16
+ ![alt text](https://github.com/alekseyl/testoscope/raw/master/results.png "results")
17
+
18
+ ## Out of the box inspections
19
+ Sequential scans, dummy One-Timer calls to DB, unused indexes
20
+
21
+ ### Sequential scans
22
+ It can happend when you are:
23
+ * truly missing an index
24
+ * when you are intend to use a partial index but unintentionally miss index condition in a query
25
+
26
+ ### One-Timers
27
+ Some times ORM can produce dummy query, in Postgres Query Plan they look like this:
28
+
29
+ QUERY PLAN
30
+ --------------------------------------------
31
+ Result (cost=0.00..0.00 rows=0 width=194)
32
+ One-Time Filter: false
33
+ (2 rows)
34
+
35
+ in SQL query you are looking for some WHERE false:
36
+
37
+ SELECT "tags".*
38
+ FROM "tags"
39
+ WHERE "tags"."parent_id" = $1 AND 1=0
40
+
41
+ and in ORM it doesn't look alarming:
42
+
43
+ sub_tags.where( name: names )
44
+
45
+ So when names is empty, we get a dummy request.
46
+
47
+ They are not a big deal from a performance perspective,
48
+ but you are occupying DB connection pool and cluttering your channel with empty noise.
49
+
50
+ So it's better to change the underlying logic other than be OK with it.
51
+
52
+ ### Unused index
53
+
54
+ Testoscope can find and warn you about unused index. Possible reasons
55
+ for them are:
56
+ * you forgot to remove index after code refactoring, and now you have redundant unused index
57
+ * you already have another index more suitable which is preferred by the planner
58
+ * your tests doesn't cover all use-cases
59
+
60
+ In either cases you may have a problem, but also may not.
61
+
62
+ ### How it works?
63
+ Testoscope hooks to exec_query of a connection adapter,
64
+ for all queries runs them two times:
65
+ first with EXPLAIN and analyze it,
66
+ and the second - is for original a caller purpose.
67
+
68
+ After achieving explain result, it simply search for configured unintended behaviour markers,
69
+ like a Seq Scan substring in Postgres QUERY PLAN explained, and also collects indexes used by all queries for a final summary.
70
+
71
+ ### Unintended Behaviours
72
+ By default unintended behaviours are preconfigured for PosgtreSQL and all tables:
73
+
74
+ config.unintened_key_words = ['Seq Scan', 'One-Time Filter']
75
+ config.tables = :all
76
+
77
+ But you can set any regexp you want to track inside EXPLAIN results,
78
+ and also you can track not all tables, but only specified ones:
79
+
80
+ config.tables = ['cards', 'users']
81
+
82
+
83
+ ## Installation
84
+
85
+ Add this line to your application's Gemfile:
86
+
87
+ ```ruby
88
+ gem 'testoscope'
89
+ ```
90
+
91
+ And then execute:
92
+
93
+ $ bundle
94
+
95
+ Or install it yourself as:
96
+
97
+ $ gem install testoscope
98
+
99
+ ## Usage
100
+
101
+ In test_helper.rb:
102
+
103
+ require 'testoscope'
104
+
105
+ #since doubling all requests to DB is not free,
106
+ #you may use ENV variable to run on demand
107
+ if ENV[:RUN_TESTOSCOPE]
108
+ Testoscope.configure { |c|
109
+ c.back_trace_paths = [Rails.root.to_s]
110
+ c.back_trace_exclude_paths = ["#{Rails.root.to_s}/test", "#{Rails.root.to_s}/spec"]
111
+ c.unintened_key_words = ['Seq Scan', 'One-Time Filter']
112
+ c.raise_when_unintended = false
113
+ c.analyze = true
114
+ c.tables = :all
115
+ }
116
+
117
+ MiniTest::Unit.after_tests {
118
+ Testoscope.print_results
119
+ }
120
+ end
121
+
122
+
123
+
124
+ ## Contributing
125
+
126
+ Bug reports and pull requests are welcome on GitHub at https://github.com/alekseyl/testoscope.
127
+
128
+ ## License
129
+
130
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "testoscope"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ module Testoscope
2
+ VERSION = "0.1.0"
3
+ end
data/lib/testoscope.rb ADDED
@@ -0,0 +1,134 @@
1
+ require "testoscope/version"
2
+
3
+ module Testoscope
4
+ class Config
5
+ attr_accessor :analyze, :back_trace_paths, :back_trace_exclude_paths,
6
+ :unintened_key_words, :analyze, :tables, :raise_when_unintended
7
+
8
+ def initialize
9
+ self.back_trace_paths = [Rails.root.to_s]
10
+ self.back_trace_exclude_paths = ["#{Rails.root.to_s}/test", "#{Rails.root.to_s}/spec"]
11
+ self.unintened_key_words = ['Seq Scan', 'One-Time Filter']
12
+ self.raise_when_unintended = false
13
+ self.analyze = true
14
+ self.tables = :all
15
+ end
16
+ end
17
+ def self.config; @@config ||= Config.new end
18
+
19
+ def self.configure
20
+ yield(config) if block_given?
21
+
22
+ ::ActiveRecord::Base.connection.class.include(AdapterUpgrade)
23
+
24
+ # since test table is small planner may want to just deal with Seq scans and skip all the index fuss
25
+ ::ActiveRecord::Base.connection.execute( 'SET enable_seqscan=off;' ) if ::ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
26
+
27
+
28
+ config.tables.map!{|table| /.*[ "]#{table}[ "].*/ } if config.tables != :all
29
+ end
30
+
31
+ def self.results
32
+ @results ||= {
33
+ unintended_behaviour: {},
34
+ indexes: {},
35
+ }
36
+ end
37
+
38
+ def self.add_unintended_behaviour( sql, explain, backtrace )
39
+ results[:unintended_behaviour][sql] ||= {}
40
+ results[:unintended_behaviour][sql][:explain] = explain
41
+ results[:unintended_behaviour][sql][:backtrace] ||= []
42
+ results[:unintended_behaviour][sql][:backtrace] << backtrace.to_a unless results[:unintended_behaviour][sql][:backtrace].include?(backtrace)
43
+ end
44
+
45
+
46
+ def self.add_index_used( sql, explain, index_used )
47
+ results[:indexes][index_used] ||= []
48
+ results[:indexes][index_used] << { explain: explain, sql: sql }
49
+ end
50
+
51
+
52
+ # Alternative way to get index names "without" ActiveRecord
53
+ #"SELECT i.relname as indname FROM pg_index as idx JOIN pg_class as i ON i.oid = idx.indexrelid
54
+ # WHERE idx.indrelid::regclass = ANY( ARRAY['#{(ActiveRecord::Base.connection.tables).join("','")}']::regclass[] )")
55
+ # .to_a.map(&:values).flatten
56
+ def self.get_all_indexes
57
+ ActiveRecord::Base.connection.tables.map { |table|
58
+ [table, ActiveRecord::Base.connection.indexes(table).map(&:name)]
59
+ }.to_h
60
+ end
61
+
62
+ def self.print_results
63
+ return yield(results) if block_given?
64
+
65
+ puts "\n<UNINTENDED BEHAVIOUR>\n" unless results[:unintended_behaviour].blank?
66
+
67
+ results[:unintended_behaviour].each do |sql, values|
68
+ puts "\nSQL:\n\n"
69
+ puts Niceql::Prettifier.prettify_sql( sql )
70
+ puts "\n\nAPP BACKTRACE:\n\n"
71
+ puts values[:backtrace].map{|arr| arr.join("\n")}.uniq.join("\n _____________________\n\n")
72
+ puts ''
73
+ puts values[:explain]
74
+ puts "\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"
75
+ end
76
+
77
+ filtered_unused_indexes = []
78
+
79
+ get_all_indexes.each do |table, indexes|
80
+ unused_indexes = indexes - results[:indexes].keys
81
+ next if unused_indexes.blank? || !sql_has_analyzing_tables?(" #{table} ")
82
+ filtered_unused_indexes << "\n-----#{table}------\n"
83
+ filtered_unused_indexes << unused_indexes
84
+ end
85
+ if !filtered_unused_indexes.blank?
86
+ puts "\n<UNUSED INDEXES>\n"
87
+ puts filtered_unused_indexes.join("\n")
88
+ end
89
+ end
90
+
91
+ def self.sql_has_analyzing_tables?(sql)
92
+ config.tables == :all || config.tables.any?{ |table| sql[table] }
93
+ end
94
+
95
+ def self.analyze( sql )
96
+ if config.analyze && sql_has_analyzing_tables?(sql)
97
+
98
+ explain = yield
99
+
100
+ app_trace = caller_locations( 2 ).map(&:to_s).select { |st|
101
+ self.config.back_trace_paths.any?{|pth| st[pth]} && !self.config.back_trace_exclude_paths.any?{|epth| st[epth]}
102
+ }
103
+ # this is the case when we for example making query from test files,
104
+ # we may omit some params for where clause and so.
105
+ return if app_trace.length == 0
106
+
107
+ unintended_found = self.config.unintened_key_words.select{|ukw| explain[ukw] }
108
+
109
+ if unintended_found.length > 0
110
+ raise StandardError.new("#{unintended_found.join(', ')} found!\n #{explain}") if config.raise_when_unintended
111
+ self.add_unintended_behaviour( sql, explain, app_trace )
112
+ end
113
+
114
+ explain.scan(/Index Scan using \w+/).each{|found| add_index_used(sql, explain, found[17..-1]) }
115
+ end
116
+ end
117
+
118
+ def self.suspend_global_analyze( analyze )
119
+ was, config.analyze = config.analyze, analyze
120
+ yield
121
+ config.analyze = was
122
+ end
123
+
124
+ module AdapterUpgrade
125
+ def exec_query(sql, name = "SQL", binds = [], prepare: false)
126
+ PGAnalyzer.analyze(sql) {
127
+ ::ActiveRecord::ConnectionAdapters::PostgreSQL::ExplainPrettyPrinter.new
128
+ .pp(super( 'EXPLAIN ' + sql, "EXPLAIN", binds, prepare: false) )
129
+ }
130
+ super( sql, name, binds, prepare: prepare )
131
+ end
132
+ end
133
+
134
+ end
data/results.png ADDED
Binary file
@@ -0,0 +1,41 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "testoscope/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "testoscope"
8
+ spec.version = Testoscope::VERSION
9
+ spec.authors = ["alekseyl"]
10
+ spec.email = ["leshchuk@gmail.com"]
11
+
12
+ spec.summary = %q{ This is simple and nice tool to inspect how application operates with current DB structure while testing app,
13
+ inspecting for redundant indexes, sequential scans, dummy requests and any other unintended behaviour customized by user. }
14
+ spec.description = %q{This is simple and nice tool to inspect how application operates with current DB structure while testing app,
15
+ meaning redundant indexes, sequential scans, dummy requests and any other unintended behaviour customized by user. }
16
+ spec.homepage = "https://github.com/alekseyl/testoscope"
17
+ spec.license = "MIT"
18
+
19
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
20
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
21
+ if spec.respond_to?(:metadata)
22
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
23
+ else
24
+ raise "RubyGems 2.0 or newer is required to protect against " \
25
+ "public gem pushes."
26
+ end
27
+
28
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
29
+ f.match(%r{^(test|spec|features)/})
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_development_dependency "bundler", "~> 1.16"
36
+ spec.add_development_dependency "rake", "~> 10.0"
37
+ spec.add_development_dependency "minitest", "~> 5.0"
38
+
39
+ spec.add_dependency "activerecord", ">= 4"
40
+ spec.add_dependency "niceql", ">= 0.1.14"
41
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: testoscope
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - alekseyl
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-03-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: niceql
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.1.14
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 0.1.14
83
+ description: "This is simple and nice tool to inspect how application operates with
84
+ current DB structure while testing app,\n meaning redundant
85
+ indexes, sequential scans, dummy requests and any other unintended behaviour customized
86
+ by user. "
87
+ email:
88
+ - leshchuk@gmail.com
89
+ executables: []
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - ".gitignore"
94
+ - ".idea/misc.xml"
95
+ - ".idea/modules.xml"
96
+ - ".idea/testoscope.iml"
97
+ - ".idea/workspace.xml"
98
+ - ".travis.yml"
99
+ - Gemfile
100
+ - LICENSE.txt
101
+ - README.md
102
+ - Rakefile
103
+ - bin/console
104
+ - bin/setup
105
+ - lib/testoscope.rb
106
+ - lib/testoscope/version.rb
107
+ - results.png
108
+ - testoscope.gemspec
109
+ homepage: https://github.com/alekseyl/testoscope
110
+ licenses:
111
+ - MIT
112
+ metadata:
113
+ allowed_push_host: https://rubygems.org
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubyforge_project:
130
+ rubygems_version: 2.6.13
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: This is simple and nice tool to inspect how application operates with current
134
+ DB structure while testing app, inspecting for redundant indexes, sequential scans,
135
+ dummy requests and any other unintended behaviour customized by user.
136
+ test_files: []