testoscope 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []