Skip to content
Snippets Groups Projects
Commit 5b2d0684 authored by Cam Saul's avatar Cam Saul
Browse files

EVERYTHING ALMOST WORKS THO :/

parent 57214c3d
No related branches found
No related tags found
No related merge requests found
......@@ -19,6 +19,7 @@
(expect-eval-actual-first 1)
(expect-expansion 0)
(expect-let 1)
(expect-when-testing-against-dataset 1)
(expect-with-all-drivers 1)
(ins 1)
(let-400 1)
......@@ -33,4 +34,5 @@
(symbol-macrolet 1)
(sync-in-context 2)
(upd 2)
(when-testing-dataset 1)
(with-credentials 1)))))))
......@@ -124,12 +124,17 @@
"Process a structured or native query, and return the result."
[query]
{:pre [(map? query)]}
(binding [qp/*query* query]
(let [driver (database-id->driver (:database query))
query (qp/preprocess query)
results (binding [qp/*query* query]
(i/process-query driver (dissoc-in query [:query :cum_sum])))] ; strip out things that individual impls don't need to know about / deal with
(qp/post-process driver query results))))
(try
(binding [qp/*query* query]
(let [driver (database-id->driver (:database query))
query (qp/preprocess query)
results (binding [qp/*query* query]
(i/process-query driver (dissoc-in query [:query :cum_sum])))] ; strip out things that individual impls don't need to know about / deal with
(qp/post-process driver query results)))
(catch Throwable e
(.printStackTrace e)
{:status :failed
:error (.getMessage e)})))
;; ## Query Execution Stuff
......
......@@ -51,21 +51,14 @@
(log/debug "Setting timezone to:" timezone)
(jdbc/db-do-prepared conn set-timezone-sql)))
(jdbc/query conn sql :as-arrays? true))]
{:status :completed
:row_count (count rows)
:data {:rows rows
:columns columns
:cols (map (fn [column first-value]
{:name column
:base_type (value->base-type first-value)})
columns first-row)}})
{:rows rows
:columns columns
:cols (map (fn [column first-value]
{:name column
:base_type (value->base-type first-value)})
columns first-row)})
(catch java.sql.SQLException e
{:status :failed
:error (or (->> (.getMessage e) ; error message comes back like 'Column "ZID" not found; SQL statement: ... [error-code]' sometimes
(re-find #"^(.*);") ; the user already knows the SQL, and error code is meaningless
second) ; so just return the part of the exception that is relevant
(.getMessage e))})))
(def db (delay (-> (sel :one Database :id 1)
db->korma-db
korma.db/get-connection)))
(throw (Exception. ^String (or (->> (.getMessage e) ; error message comes back like 'Column "ZID" not found; SQL statement: ... [error-code]' sometimes
(re-find #"^(.*);") ; the user already knows the SQL, and error code is meaningless
second) ; so just return the part of the exception that is relevant
(.getMessage e)))))))
......@@ -45,11 +45,10 @@
eval
(annotate/annotate query))
(catch java.sql.SQLException e
{:status :failed
:error (or (->> (.getMessage e) ; error message comes back like "Error message ... [status-code]" sometimes
(re-find #"(?s)(^.*)\s+\[[\d-]+\]$") ; status code isn't useful and makes unit tests hard to write so strip it off
second)
(.getMessage e))}))) ; (?s) = Pattern.DOTALL - tell regex `.` to match newline characters as well
(throw (Exception. ^String (or (->> (.getMessage e) ; error message comes back like "Error message ... [status-code]" sometimes
(re-find #"(?s)(^.*)\s+\[[\d-]+\]$") ; status code isn't useful and makes unit tests hard to write so strip it off
second) ; (?s) = Pattern.DOTALL - tell regex `.` to match newline characters as well
(.getMessage e)))))))
(defn process-and-run
......
......@@ -188,12 +188,12 @@
(defn post-process
"Apply post-processing steps to the RESULTS of a QUERY, such as applying cumulative sum."
[driver query results]
(case (keyword (:type query))
:native results
:query (let [query (:query query)]
(->> results
(post-process-cumulative-sum query)
add-row-count-and-status))))
(->> (case (keyword (:type query))
:native results
:query (let [query (:query query)]
(->> results
(post-process-cumulative-sum query))))
add-row-count-and-status))
;; # COMMON ANNOTATION FNS
......
......@@ -5,6 +5,7 @@
[metabase.driver.mongo.test-data :as mongo-test-data]
(metabase.models [database :refer [Database]]
[table :refer [Table]])
[metabase.test.data.datasets :as datasets]
[metabase.test-data :refer :all]
[metabase.test.util :refer [match-$ random-name expect-eval-actual-first]]))
......@@ -118,36 +119,39 @@
:logo_url nil
:report_timezone nil
:inherits true}]
[(match-$ (sel :one Database :name db-name)
{:created_at $
:engine "postgres"
:id $
:details {:conn_str "host=localhost port=5432 dbname=fakedb user=cam"}
:updated_at $
:organization org
:name $
:organization_id @org-id
:description nil})
(match-$ @mongo-test-data/mongo-test-db
{:created_at $
:engine "mongo"
:id $
:details $
:updated_at $
:organization org
:name "Mongo Test"
:organization_id @org-id
:description nil})
(match-$ @test-db
{:created_at $
:engine "h2"
:id $
:details $
:updated_at $
:organization org
:name "Test Database"
:organization_id @org-id
:description nil})])
(filter identity
[(datasets/when-testing-dataset :generic-sql
(match-$ (sel :one Database :name db-name)
{:created_at $
:engine "postgres"
:id $
:details {:conn_str "host=localhost port=5432 dbname=fakedb user=cam"}
:updated_at $
:organization org
:name $
:organization_id @org-id
:description nil}))
(datasets/when-testing-dataset :mongo
(match-$ @mongo-test-data/mongo-test-db
{:created_at $
:engine "mongo"
:id $
:details $
:updated_at $
:organization org
:name "Mongo Test"
:organization_id @org-id
:description nil}))
(match-$ @test-db
{:created_at $
:engine "h2"
:id $
:details $
:updated_at $
:organization org
:name "Test Database"
:organization_id @org-id
:description nil})]))
(do
;; Delete all the randomly created Databases we've made so far
(cascade-delete Database :organization_id @org-id :id [not-in (set [@db-id
......
......@@ -8,6 +8,7 @@
(metabase.models [field :refer [Field]]
[foreign-key :refer [ForeignKey]]
[table :refer [Table]])
[metabase.test.data.datasets :as datasets, :refer [*dataset* with-dataset-when-testing]]
[metabase.test-data :refer :all]
[metabase.test.util :refer [match-$ expect-eval-actual-first]]))
......@@ -22,14 +23,14 @@
;; ## GET /api/meta/table?org
;; These should come back in alphabetical order and include relevant metadata
(expect #{{:name "CATEGORIES", :rows 75, :active true, :id (table->id :categories), :db_id @db-id}
{:name "CHECKINS", :rows 1000, :active true, :id (table->id :checkins), :db_id @db-id}
{:name "USERS", :rows 15, :active true, :id (table->id :users), :db_id @db-id}
{:name "VENUES", :rows 100, :active true, :id (table->id :venues), :db_id @db-id}
{:name "categories", :rows 75, :active true, :id (mongo-data/table-name->id :categories), :db_id @mongo-test-db-id}
{:name "checkins", :rows 1000, :active true, :id (mongo-data/table-name->id :checkins), :db_id @mongo-test-db-id}
{:name "users", :rows 15, :active true, :id (mongo-data/table-name->id :users), :db_id @mongo-test-db-id}
{:name "venues", :rows 100, :active true, :id (mongo-data/table-name->id :venues), :db_id @mongo-test-db-id}}
(expect (set (mapcat (fn [dataset-name]
(with-dataset-when-testing dataset-name
(let [db-id (:id (datasets/db *dataset*))]
[{:name (datasets/format-name *dataset* "categories"), :db_id db-id, :active true, :rows 75, :id (datasets/table-name->id *dataset* :categories)}
{:name (datasets/format-name *dataset* "checkins"), :db_id db-id, :active true, :rows 1000, :id (datasets/table-name->id *dataset* :checkins)}
{:name (datasets/format-name *dataset* "users"), :db_id db-id, :active true, :rows 15, :id (datasets/table-name->id *dataset* :users)}
{:name (datasets/format-name *dataset* "venues"), :db_id db-id, :active true, :rows 100, :id (datasets/table-name->id *dataset* :venues)}])))
@datasets/test-dataset-names))
(->> ((user->client :rasta) :get 200 "meta/table" :org @org-id)
(map #(dissoc % :db :created_at :updated_at :entity_name :description :entity_type))
set))
......
(ns metabase.driver.generic-sql.native-test
(:require [expectations :refer :all]
[metabase.driver.generic-sql.native :refer :all]
[metabase.driver :as driver]
[metabase.test-data :refer :all]))
;; Just check that a basic query works
......@@ -10,8 +10,8 @@
[99]]
:columns [:id]
:cols [{:name :id, :base_type :IntegerField}]}}
(process-and-run {:native {:query "SELECT ID FROM VENUES ORDER BY ID DESC LIMIT 2;"}
:database @db-id}))
(driver/process-query {:native {:query "SELECT ID FROM VENUES ORDER BY ID DESC LIMIT 2;"}
:database @db-id}))
;; Check that column ordering is maintained
(expect
......@@ -23,11 +23,11 @@
:cols [{:name :id, :base_type :IntegerField}
{:name :name, :base_type :TextField}
{:name :category_id, :base_type :IntegerField}]}}
(process-and-run {:native {:query "SELECT ID, NAME, CATEGORY_ID FROM VENUES ORDER BY ID DESC LIMIT 2;"}
:database @db-id}))
(driver/process-query {:native {:query "SELECT ID, NAME, CATEGORY_ID FROM VENUES ORDER BY ID DESC LIMIT 2;"}
:database @db-id}))
;; Check that we get proper error responses for malformed SQL
(expect {:status :failed
:error "Column \"ZID\" not found"}
(process-and-run {:native {:query "SELECT ZID FROM CHECKINS LIMIT 2;"}
:database @db-id}))
(driver/process-query {:native {:query "SELECT ZID FROM CHECKINS LIMIT 2;"}
:database @db-id}))
......@@ -39,22 +39,27 @@
^{:doc "A delay that fetches or creates the Mongo test `Database`.
If DB is created, `load-data` and `sync-database!` are called to get the DB in a state that we can use for testing."}
mongo-test-db
(delay (let [db (or (sel :one Database :name mongo-test-db-name)
(let [db (ins Database
:organization_id @org-id
:name mongo-test-db-name
:engine :mongo
:details {:conn_str mongo-test-db-conn-str})]
(log/info (color/cyan "Loading Mongo test data..."))
(load-data)
(driver/sync-database! db)
(set-field-special-types!)
(log/info (color/cyan "Done."))
db))]
(assert (and (map? db)
(integer? (:id db))
(exists? Database :id (:id db))))
db)))
(delay
;; Resolve metabase.test.data.datasets at runtime to avoid circular dependency
(require 'metabase.test.data.datasets)
(assert (contains? @@(ns-resolve 'metabase.test.data.datasets 'test-dataset-names) :mongo)
"Why are we attempting to use the Mongo test Database when we're not testing against mongo?")
(let [db (or (sel :one Database :name mongo-test-db-name)
(let [db (ins Database
:organization_id @org-id
:name mongo-test-db-name
:engine :mongo
:details {:conn_str mongo-test-db-conn-str})]
(log/info (color/cyan "Loading Mongo test data..."))
(load-data)
(driver/sync-database! db)
(set-field-special-types!)
(log/info (color/cyan "Done."))
db))]
(assert (and (map? db)
(integer? (:id db))
(exists? Database :id (:id db))))
db)))
(defonce
^{:doc "A Delay that returns the ID of `mongo-test-db`, forcing creation of it if needed."}
......
......@@ -10,8 +10,16 @@
(metabase.models [field :refer [Field]]
[table :refer [Table]])
[metabase.test-data.data :refer [test-data]]
[metabase.test.data.datasets :as datasets]
[metabase.test.util :refer [expect-eval-actual-first resolve-private-fns]]))
;; ## Logic for selectively running mongo
(defmacro expect-when-testing-mongo [expected actual]
`(datasets/expect-when-testing-dataset :mongo
~expected
~actual))
;; ## Constants + Helper Fns/Macros
;; TODO - move these to metabase.test-data ?
(def ^:private ^:const table-names
......@@ -56,35 +64,35 @@
;; ## Tests for connection functions
(expect true
(expect-when-testing-mongo true
(driver/can-connect? @mongo-test-db))
(expect false
(expect-when-testing-mongo false
(driver/can-connect? {:engine :mongo
:details {:conn_str "mongodb://123.4.5.6/bad-db-name?connectTimeoutMS=50"}})) ; timeout after 50ms instead of 10s so test's don't take forever
(expect false
(expect-when-testing-mongo false
(driver/can-connect? {:engine :mongo
:details {:conn_str "mongodb://localhost:3000/bad-db-name?connectTimeoutMS=50"}}))
(expect false
(expect-when-testing-mongo false
(driver/can-connect-with-details? :mongo {}))
(expect true
(expect-when-testing-mongo true
(driver/can-connect-with-details? :mongo {:host "localhost"
:port 27017
:dbname "metabase-test"}))
;; should use default port 27017 if not specified
(expect true
(expect-when-testing-mongo true
(driver/can-connect-with-details? :mongo {:host "localhost"
:dbname "metabase-test"}))
(expect false
(expect-when-testing-mongo false
(driver/can-connect-with-details? :mongo {:host "123.4.5.6"
:dbname "bad-db-name?connectTimeoutMS=50"}))
(expect false
(expect-when-testing-mongo false
(driver/can-connect-with-details? :mongo {:host "localhost"
:port 3000
:dbname "bad-db-name?connectTimeoutMS=50"}))
......@@ -95,12 +103,12 @@
(resolve-private-fns metabase.driver.mongo field->base-type table->column-names)
;; ### active-table-names
(expect
(expect-when-testing-mongo
#{"checkins" "categories" "users" "venues"}
(i/active-table-names mongo/driver @mongo-test-db))
;; ### table->column-names
(expect
(expect-when-testing-mongo
[#{:_id :name}
#{:_id :date :venue_id :user_id}
#{:_id :name :last_login}
......@@ -110,7 +118,7 @@
(map table->column-names)))
;; ### field->base-type
(expect
(expect-when-testing-mongo
[:IntegerField ; categories._id
:TextField ; categories.name
:IntegerField ; checkins._id
......@@ -131,7 +139,7 @@
(mapv field->base-type)))
;; ### active-column-names->type
(expect
(expect-when-testing-mongo
[{"_id" :IntegerField, "name" :TextField}
{"_id" :IntegerField, "date" :DateField, "venue_id" :IntegerField, "user_id" :IntegerField}
{"_id" :IntegerField, "name" :TextField, "last_login" :DateField}
......@@ -141,7 +149,7 @@
(mapv (partial i/active-column-names->type mongo/driver))))
;; ### table-pks
(expect
(expect-when-testing-mongo
[#{"_id"} #{"_id"} #{"_id"} #{"_id"}] ; _id for every table
(->> table-names
(map table-name->fake-table)
......@@ -151,7 +159,7 @@
;; ## Big-picture tests for the way data should look post-sync
;; Test that Tables got synced correctly, and row counts are correct
(expect
(expect-when-testing-mongo
[{:rows 75, :active true, :name "categories"}
{:rows 1000, :active true, :name "checkins"}
{:rows 15, :active true, :name "users"}
......@@ -159,7 +167,7 @@
(sel :many :fields [Table :name :active :rows] :db_id @mongo-test-db-id (k/order :name)))
;; Test that Fields got synced correctly, and types are correct
(expect
(expect-when-testing-mongo
[[{:special_type :id, :base_type :IntegerField, :name "_id"}
{:special_type :category, :base_type :DateField, :name "last_login"}
{:special_type :category, :base_type :TextField, :name "name"}]
......
......@@ -4,67 +4,37 @@
[metabase.db :refer :all]
[metabase.driver :as driver]
(metabase.models [table :refer [Table]])
[metabase.test.data.datasets :as datasets]))
;; ## Functionality for writing driver-independent tests
(def ^:dynamic *db*
"Bound to `Database` for the current driver inside body of `with-dataset`.")
(def ^:dynamic *db-id*
"Bound to ID of `Database` for the current driver inside body of `with-dataset`.")
(def ^:dynamic *driver-dataset*)
;; ### EXPECT-WITH-ALL-DRIVERS
(defmacro with-dataset
"Execute BODY with `*driver-dataset*`, `*db*` and `*db-id*` bound to appropriate values for DRIVER-NAME."
[dataset-name & body]
{:pre [(keyword? dataset-name)
(contains? datasets/all-valid-dataset-names dataset-name)]}
`(let [dataset# (datasets/dataset-name->dataset ~dataset-name)
db# (datasets/db dataset#)]
(binding [*driver-dataset* dataset#
*db* db#
*db-id* (:id db#)]
(assert (and (integer? *db-id*)
(map? *db*)))
~@body)))
(defmacro expect-with-all-datasets
"Like expect, but runs a test inside of `with-dataset` for *each* of the available datasets."
[expected actual]
`(do ~@(mapcat (fn [dataset-name]
`[(expect
(with-dataset ~dataset-name
~expected)
(with-dataset ~dataset-name
~actual))])
@datasets/test-dataset-names)))
[metabase.test.data.datasets :as datasets :refer [*dataset* expect-with-all-datasets]]))
;; ## Dataset-Independent Data Fns
(defn ->table
"Given keyword TABLE-NAME, fetch corresponding `Table` in `*db*`."
[table-name]
{:pre [*driver-dataset*]
:post [(map? %)]}
(datasets/table-name->table *driver-dataset* table-name))
(defn id
"Return the ID of a `Table` or `Field` for the current driver data set."
([table-name]
{:pre [*driver-dataset*
{:pre [*dataset*
(keyword? table-name)]
:post [(integer? %)]}
(:id (->table table-name)))
(datasets/table-name->id *dataset* table-name))
([table-name field-name]
{:pre [*driver-dataset*
{:pre [*dataset*
(keyword? table-name)
(keyword? field-name)]
:post [(integer? %)]}
(datasets/field-name->id *driver-dataset* table-name field-name)))
(datasets/field-name->id *dataset* table-name field-name)))
(defn db-id []
{:pre [*dataset*]
:post [(integer? %)]}
(:id (datasets/db *dataset*)))
(defn fks-supported? []
(datasets/fks-supported? *dataset*))
(defn format-name [name]
(datasets/format-name *dataset* name))
(defn id-field-type []
(datasets/id-field-type *dataset*))
;; ## Dataset-Independent QP Tests
......@@ -79,18 +49,9 @@
:row_count ~(count (:rows data))
:data ~data}
(driver/process-query {:type :query
:database *db-id*
:database (db-id)
:query ~query})))
(defn fks-supported? []
(datasets/fks-supported? *driver-dataset*))
(defn format-name [name]
(datasets/format-name *driver-dataset* name))
(defn id-field-type []
(datasets/id-field-type *driver-dataset*))
(defn ->columns
"Generate the vector that should go in the `columns` part of a QP result; done by calling `format-name` against each column name."
[& names]
......@@ -547,7 +508,7 @@
:row_count 0
:data {:rows [], :columns [], :cols []}}
(driver/process-query {:type :query
:database *db-id*
:database (db-id)
:native {}
:query {:source_table 0
:filter [nil nil]
......
......@@ -4,6 +4,7 @@
[clojure.tools.logging :as log]
[colorize.core :as color]
[environ.core :refer [env]]
[expectations :refer :all]
[metabase.driver.mongo.test-data :as mongo-data]
[metabase.test-data :as generic-sql-data]))
......@@ -17,6 +18,8 @@
"Return `Database` containing test data for this driver.")
(table-name->table [this table-name]
"Given a TABLE-NAME keyword like `:venues`, *fetch* the corresponding `Table`.")
(table-name->id [this table-name]
"Given a TABLE-NAME keyword like `:venues`, return corresponding `Table` ID.")
(field-name->id [this table-name field-name]
"Given keyword TABLE-NAME and FIELD-NAME, return the corresponding `Field` ID.")
(fks-supported? [this]
......@@ -38,11 +41,12 @@
(mongo-data/destroy!)
@mongo-data/mongo-test-db
(assert (integer? @mongo-data/mongo-test-db-id)))
(db [_]
@mongo-data/mongo-test-db)
(table-name->table [_ table-name]
(mongo-data/table-name->table table-name))
(table-name->id [_ table-name]
(mongo-data/table-name->id table-name))
(field-name->id [_ table-name field-name]
(mongo-data/field-name->id table-name (if (= field-name :id) :_id
field-name)))
......@@ -51,7 +55,6 @@
(format-name [_ table-or-field-name]
(if (= table-or-field-name "id") "_id"
table-or-field-name))
(id-field-type [_]
:IntegerField))
......@@ -63,19 +66,18 @@
(load-data! [_]
@generic-sql-data/test-db
(assert (integer? @generic-sql-data/db-id)))
(db [_]
@generic-sql-data/test-db)
(table-name->table [_ table-name]
(generic-sql-data/table-name->table table-name))
(table-name->id [_ table-name]
(generic-sql-data/table->id table-name))
(field-name->id [_ table-name field-name]
(generic-sql-data/field->id table-name field-name))
(fks-supported? [_]
true)
(format-name [_ table-or-field-name]
(clojure.string/upper-case table-or-field-name))
(id-field-type [_]
:BigIntegerField))
......@@ -124,3 +126,61 @@
all-valid-dataset-names)]
(log/info (color/green "Running QP tests against these datasets: " datasets))
datasets)))
;; # Helper Macros
(def ^:dynamic *dataset*
"The dataset we're currently testing against, bound by `with-dataset`."
nil)
(defmacro with-dataset
"Bind `*dataset*` to the dataset with DATASET-NAME and execute BODY."
[dataset-name & body]
`(binding [*dataset* (dataset-name->dataset ~dataset-name)]
~@body))
(defmacro when-testing-dataset
"Execute BODY only if we're currently testing against DATASET-NAME."
[dataset-name & body]
`(when (contains? @test-dataset-names ~dataset-name)
~@body))
(defmacro with-dataset-when-testing
"When testing DATASET-NAME, binding `*dataset*` and executes BODY."
[dataset-name & body]
`(when-testing-dataset ~dataset-name
(with-dataset ~dataset-name
~@body)))
(defmacro expect-when-testing-dataset
"Generate a unit test that only runs if we're currently testing against DATASET-NAME."
[dataset-name expected actual]
`(expect
(when-testing-dataset ~dataset-name
~expected)
(when-testing-dataset ~dataset-name
~actual)))
(defmacro expect-with-dataset
"Generate a unit test that only runs if we're currently testing against DATASET-NAME, and that binds `*dataset*` to the current dataset."
[dataset-name expected actual]
`(expect-when-testing-dataset ~dataset-name
(with-dataset ~dataset-name
~expected)
(with-dataset ~dataset-name
~actual)))
(defmacro expect-with-datasets
"Generate unit tests for all datasets in DATASET-NAMES; each test will only run if we're currently testing the corresponding dataset.
`*dataset*` is bound to the current dataset inside each test."
[dataset-names expected actual]
`(do ~@(map (fn [dataset-name]
`(expect-with-dataset ~dataset-name ~expected ~actual))
dataset-names)))
(defmacro expect-with-all-datasets
"Generate unit tests for all valid datasets; each test will only run if we're currently testing the corresponding dataset.
`*dataset*` is bound to the current dataset inside each test."
[expected actual]
`(expect-with-datasets ~all-valid-dataset-names ~expected ~actual))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment