Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ae6392a82 | ||
|
|
ac16a94a92 | ||
|
|
65f5bb2daf | ||
|
|
22810a96b7 | ||
|
|
638fd6234e | ||
|
|
6945843c97 | ||
|
|
6d53606b22 | ||
|
|
789719ada6 | ||
|
|
7014ab4d01 | ||
|
|
790833dc4e | ||
|
|
f575441aa6 | ||
|
|
e4b13b753b | ||
|
|
76622dc32e | ||
|
|
80f2641bae | ||
|
|
186f132262 | ||
|
|
00a1ebaf25 | ||
|
|
09389e2ee4 | ||
|
|
5d2412bbb0 | ||
|
|
174a74023d | ||
|
|
d607a76097 | ||
|
|
34056c1c66 | ||
|
|
c58138b4cc | ||
|
|
1d50e2f0bf | ||
|
|
c57b477dc9 | ||
|
|
86f63eecc5 | ||
|
|
ff25927b52 | ||
|
|
141e1a938d | ||
|
|
bab4f304c3 | ||
|
|
5ecb66a4ef | ||
|
|
5ae5907d11 | ||
|
|
24c5ecc437 | ||
|
|
0a144a633d | ||
|
|
f308d1cfde | ||
|
|
3f4057b17c | ||
|
|
4b4f778360 | ||
|
|
6edc384c05 | ||
|
|
540613645f | ||
|
|
586605557e | ||
|
|
f08c9d36a6 | ||
|
|
da43314332 | ||
|
|
ccd5455103 | ||
|
|
d3d408bc68 | ||
|
|
a9bafda41c | ||
|
|
3d1fd4a7aa | ||
|
|
951bec8596 | ||
|
|
5e47e8bd29 | ||
|
|
d53c94eb91 | ||
|
|
111d2cecf9 | ||
|
|
eccb430a9b | ||
|
|
bd207a3551 | ||
|
|
d3f12802a3 | ||
|
|
218394fefc | ||
|
|
f036d41559 | ||
|
|
ac5c18e261 | ||
|
|
e3f5cab6dd | ||
|
|
9bfaae7194 | ||
|
|
4844270f0b | ||
|
|
3167d96cfc | ||
|
|
84f1bd95e7 | ||
|
|
9766bb2ed2 | ||
|
|
3af20f9327 | ||
|
|
983f285373 | ||
|
|
f0f447857f | ||
|
|
fb66acebc3 | ||
|
|
508e49e986 | ||
|
|
44fe84a58a | ||
|
|
0bccd3a801 | ||
|
|
2721a90455 | ||
|
|
52bd1ed1d4 | ||
|
|
68c2b4d084 | ||
|
|
ab94dd5b86 | ||
|
|
556e7d445a | ||
|
|
fbaa61bbbc | ||
|
|
8f4c5aab0f | ||
|
|
a4084cb556 | ||
|
|
9ffa05affb | ||
|
|
07d390b72e | ||
|
|
5690518f25 | ||
|
|
79b2d46849 | ||
|
|
9c7eaf63d5 | ||
|
|
0ce796e1a4 | ||
|
|
b5203b5c0d | ||
|
|
b14b1cd60d | ||
|
|
0c0d0ea3a3 | ||
|
|
d12b686e52 | ||
|
|
73185940b6 | ||
|
|
f3920afe8c | ||
|
|
ad1994574d | ||
|
|
d772ae0b31 | ||
|
|
ed94244741 | ||
|
|
73cb979721 | ||
|
|
b5d6368cca | ||
|
|
be3b954716 | ||
|
|
1d05cfa0af | ||
|
|
38c971ab49 | ||
|
|
61523e33df | ||
|
|
9b5764d4fb | ||
|
|
1bc824e39b | ||
|
|
e662e907be | ||
|
|
28178e46de | ||
|
|
3acc6febae | ||
|
|
fbb8ffbfc6 | ||
|
|
581a2f164b | ||
|
|
051d7d6eb4 | ||
|
|
7766c3bb66 | ||
|
|
35aa91de27 | ||
|
|
3a793ed87d | ||
|
|
9cfdf4a194 | ||
|
|
aa0ffedc8c | ||
|
|
82c162d399 | ||
|
|
995d52db1b | ||
|
|
c7e58f0828 | ||
|
|
1f99576a37 | ||
|
|
91d8d3ee83 | ||
|
|
5a92c5508c | ||
|
|
73a97893e9 | ||
|
|
bc1f876fb5 | ||
|
|
eeb0fdf3a4 | ||
|
|
4fb24cb229 | ||
|
|
d35b3661a9 | ||
|
|
3e6e223ed0 | ||
|
|
8579edf1ca | ||
|
|
420d85e811 | ||
|
|
924a46894b | ||
|
|
71aec1f07a | ||
|
|
4688e62b90 | ||
|
|
232790b452 | ||
|
|
4c1bb758ce | ||
|
|
6749b2ace8 | ||
|
|
8f7fdcbdca | ||
|
|
1e50471a87 | ||
|
|
20a5728152 | ||
|
|
3170fb22cb | ||
|
|
c02d15ec34 | ||
|
|
2a4edaf08f | ||
|
|
639ca96caa | ||
|
|
fb13ce8b07 | ||
|
|
2aacab44ec | ||
|
|
279acc8af8 | ||
|
|
3765728342 | ||
|
|
68ceb212b5 | ||
|
|
e52114e969 | ||
|
|
e3b7dd013b | ||
|
|
cea3b6b43e | ||
|
|
06d39e0ab7 | ||
|
|
785108002e | ||
|
|
c12853d0b0 | ||
|
|
f1afa0ac33 | ||
|
|
5265016003 | ||
|
|
c6cb3db624 | ||
|
|
d35f58a2b5 | ||
|
|
d9ada2e58a | ||
|
|
4d6c688bf0 | ||
|
|
3eec0947d2 | ||
|
|
5979272115 | ||
|
|
5fb70ace3c | ||
|
|
5e4398ceae | ||
|
|
67a9820c98 | ||
|
|
665f79d26c | ||
|
|
6152819e45 | ||
|
|
427a08029a | ||
|
|
df7ef6d8f0 | ||
|
|
b9b646bb11 | ||
|
|
ae62f8ea3d | ||
|
|
0939632399 | ||
|
|
4b5a2b96a8 | ||
|
|
79bf430dee | ||
|
|
62a168d44f | ||
|
|
502c8dae91 | ||
|
|
afdfc54802 | ||
|
|
651b65a882 | ||
|
|
c2290acae3 | ||
|
|
e1276afd71 | ||
|
|
dea0fb8b78 | ||
|
|
faebfb3e62 | ||
|
|
6fd7b7d427 | ||
|
|
385b415676 | ||
|
|
50f0f5f6dc | ||
|
|
8889847c71 | ||
|
|
d26bb75ebc | ||
|
|
a65cca44e5 | ||
|
|
18b0260d1f | ||
|
|
c9d4ef5e89 | ||
|
|
c0f3f027ae | ||
|
|
0bd5360315 | ||
|
|
29b46ba59c | ||
|
|
04fe0023b7 | ||
|
|
cb3c3e0e2e | ||
|
|
0ac9d72deb | ||
|
|
dd2200bdb8 | ||
|
|
ca894c621f | ||
|
|
4224b3d326 | ||
|
|
891f45c1ca | ||
|
|
595f51d76c | ||
|
|
50cdf99913 | ||
|
|
e9f2f8eb83 | ||
|
|
73d2aa8fe1 | ||
|
|
579eaf9141 | ||
|
|
c513930e27 | ||
|
|
7a75447364 | ||
|
|
ec6bb5590d | ||
|
|
637e7a4e24 | ||
|
|
08a70baf3f | ||
|
|
bc43ca6f0f | ||
|
|
65ba084d80 | ||
|
|
961ad31c13 | ||
|
|
40509cc5fd | ||
|
|
7606a88293 | ||
|
|
d3beb57f15 | ||
|
|
691106326f | ||
|
|
c82205ce4f | ||
|
|
f0552ab72d | ||
|
|
786081aafa | ||
|
|
0251a8bb73 | ||
|
|
2a78500fdf | ||
|
|
f68d81ff17 | ||
|
|
38ecb5ffc4 | ||
|
|
fa8f3a2997 | ||
|
|
00f4413f12 | ||
|
|
d4d97edeef | ||
|
|
14cc8d5c24 | ||
|
|
cc983f9ad6 | ||
|
|
0764b9263d | ||
|
|
a207b66b7e | ||
|
|
d42bd2e803 | ||
|
|
27b3cac1e0 | ||
|
|
866cdccd49 | ||
|
|
1e58c94598 | ||
|
|
96bcaa5baa | ||
|
|
fe9d7e459d | ||
|
|
3f21160545 | ||
|
|
e4e7287441 | ||
|
|
f78c83188f | ||
|
|
a57c9e79d9 | ||
|
|
772e854aa6 | ||
|
|
717bc0d919 | ||
|
|
c64d58cee9 | ||
|
|
a5dc06f217 | ||
|
|
631ef974b6 | ||
|
|
a749d6ce13 | ||
|
|
1bd5e1f04b | ||
|
|
5467b07823 | ||
|
|
ead8f0e449 | ||
|
|
0860a6b704 | ||
|
|
573737a9ae | ||
|
|
076c40e624 | ||
|
|
dc60ae89da | ||
|
|
d68bea75f0 | ||
|
|
9968b088ea | ||
|
|
bc47ccc818 | ||
|
|
96f8389751 | ||
|
|
1c9d7807cf | ||
|
|
c9e88c86be | ||
|
|
10ad49b766 | ||
|
|
1ba7bee5bf | ||
|
|
c6793cc454 | ||
|
|
691ebfbefb | ||
|
|
c096891efa | ||
|
|
22f0b674e2 | ||
|
|
d8cf69c8e9 | ||
|
|
e27ad3f9b1 | ||
|
|
da58a9b0a6 | ||
|
|
165f411083 | ||
|
|
0bb942857a | ||
|
|
58a9c2b2bf | ||
|
|
8b3c5bd8e9 | ||
|
|
f545746e7e | ||
|
|
1123f24fb8 | ||
|
|
9b0b71363f | ||
|
|
2e5a006846 | ||
|
|
72df273e10 | ||
|
|
7b8559e32a | ||
|
|
002f61d570 | ||
|
|
a72ade041b | ||
|
|
0cf0ecc5e8 | ||
|
|
42ef52d271 | ||
|
|
e6a2af1cd4 | ||
|
|
8b2ff2b57d | ||
|
|
23aca09afc | ||
|
|
00fa57d9c4 | ||
|
|
5f7b18fe0e | ||
|
|
fc5d7c2045 | ||
|
|
e5ef5b93e1 | ||
|
|
081f0f7ebc | ||
|
|
fadfaeb547 | ||
|
|
95c804b5b5 | ||
|
|
6f4924878a | ||
|
|
83429723ce | ||
|
|
a9c482a7c6 | ||
|
|
dada1ab0c1 | ||
|
|
7859870646 | ||
|
|
58ee238db4 | ||
|
|
09ad707d94 | ||
|
|
dfefaded1f | ||
|
|
6585c3be40 | ||
|
|
d0a6190887 | ||
|
|
73ee7ca95a | ||
|
|
d904ea9a29 | ||
|
|
e0dd4c6a6f | ||
|
|
7c85c867de | ||
|
|
2bc16d6c1b | ||
|
|
d93ec7e56d | ||
|
|
de22b80203 | ||
|
|
4086215a03 | ||
|
|
fb2c7ed00a | ||
|
|
1006cbc9a7 | ||
|
|
64b074881e | ||
|
|
10e5cfce49 | ||
|
|
f2e6be81d2 | ||
|
|
6ceb5fc5ed | ||
|
|
5b073c4978 | ||
|
|
52c6e599f7 | ||
|
|
500de36308 | ||
|
|
d33fb4c9ca | ||
|
|
702be10567 | ||
|
|
8fae96dcbd | ||
|
|
403701b258 | ||
|
|
2c2b6393c0 | ||
|
|
6b7ebb2111 | ||
|
|
cce904ba6d | ||
|
|
e93e156c5f | ||
|
|
7d42cd3f07 | ||
|
|
590d47906e | ||
|
|
af75cf55b5 | ||
|
|
b376588d54 | ||
|
|
b003385ed2 | ||
|
|
742384327c | ||
|
|
bbdd0affba | ||
|
|
8f5f280597 | ||
|
|
fbbcd0ad58 | ||
|
|
209230dfd3 | ||
|
|
0d71a34ff9 | ||
|
|
4baa5ec3c6 | ||
|
|
22bcfafa76 | ||
|
|
09aa95fe2e | ||
|
|
adcb1e0d49 | ||
|
|
d3a6757d5e | ||
|
|
f49735094b | ||
|
|
5020ef10ea | ||
|
|
c11eee546d | ||
|
|
96d7d6eb94 | ||
|
|
cd58573730 | ||
|
|
159f589c0c | ||
|
|
597428b6c5 | ||
|
|
e071532d35 | ||
|
|
b771465d99 | ||
|
|
45f42dbb5e | ||
|
|
27104c05f1 | ||
|
|
82307cde6c | ||
|
|
5ca2abb8d0 | ||
|
|
e1684fc8c0 | ||
|
|
8c605dcd96 | ||
|
|
ff88e7f2f9 | ||
|
|
27e192440e | ||
|
|
a3dfdf7622 | ||
|
|
2393b81174 |
@@ -0,0 +1 @@
|
||||
bindata.go -diff
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
.DS_Store
|
||||
pgweb
|
||||
bin
|
||||
|
||||
tmp/
|
||||
cover.out
|
||||
+21
-5
@@ -1,8 +1,24 @@
|
||||
sudo: required
|
||||
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.3
|
||||
- tip
|
||||
services:
|
||||
- docker
|
||||
|
||||
install: make setup
|
||||
script: make dev
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
|
||||
go:
|
||||
- 1.5.4
|
||||
- 1.6.2
|
||||
|
||||
env:
|
||||
- GO15VENDOREXPERIMENT=1
|
||||
|
||||
install:
|
||||
- make setup
|
||||
|
||||
script:
|
||||
- make build
|
||||
- make test
|
||||
- PGHOST=127.0.0.1 ./script/test_all.sh
|
||||
+134
-5
@@ -1,4 +1,133 @@
|
||||
## 0.4.0
|
||||
## 0.9.3 - 2016-06-30
|
||||
|
||||
- Uses Go 1.6 for development, GH-155
|
||||
- Fixes timestamp formatting in CSV export, GH-163
|
||||
- Included PostgreSQL 9.6 for integration testing
|
||||
- Switches docker image to Alpine to reduce image size
|
||||
- Adds support for ARMv5
|
||||
|
||||
## 0.9.2 - 2016-03-01
|
||||
|
||||
- Fixes bug with unsafe base64 encoded sql queries
|
||||
- Fixes issue with session id not being included in multi-session mode
|
||||
- Fixes visual issue with long table names in sidebar
|
||||
- Fixes visual issue with a scrollbar in table information widget
|
||||
- Fixes issue with database connection form being reset by clicking on 'cancel' button
|
||||
- Adds ability to close connection
|
||||
- Adds display message for number of affected rows for update/delete queries, GH-133
|
||||
- Adds web server url prefix as a CLI option, GH-135
|
||||
|
||||
## 0.9.1 - 2016-01-25
|
||||
|
||||
- Fixes bug with tables context menu
|
||||
- Fixes JS bug when query returns no rows
|
||||
- Fixes bug with switching between different connection modes
|
||||
- Adds AJAX timeout to 5s
|
||||
- Adds sidebar reload action on any CREATE/DROP action
|
||||
|
||||
## 0.9.0 - 2016-01-19
|
||||
|
||||
- Add support for multiple schemas. GH-112
|
||||
- Add support for native ssh tunnes. GH-114
|
||||
- Add materialized views to list of schema objects
|
||||
- Adds a few design tweaks and cleanups
|
||||
- Fixes bug with nil result set when fetching rows
|
||||
|
||||
## 0.8.0 - 2016-01-11
|
||||
|
||||
- Fixes bug with bigint conversions in javascript. Now bigints are encoded as strings. GH-109
|
||||
- Adds pagination and simple column filtering to table rows browser. GH-110
|
||||
- Adds ability to use pgweb with multiple database sessions. GH-111
|
||||
- Adds a few design tweaks and cleanups
|
||||
|
||||
## 0.7.0 - 2016-01-05
|
||||
|
||||
- Adds sequences to the sidebar panel - GH-100
|
||||
- Adds table constrains view - GH-104
|
||||
- Adds ability to export table and query rows as JSON/XML - GH-107
|
||||
- Updates to UI theme and SQL editor
|
||||
|
||||
## 0.6.3 - 2015-08-16
|
||||
|
||||
- Adds PostgreSQL password escaping in web ui, GH-96
|
||||
- Adds base64 query encoding for CSV export, GH-95
|
||||
- Adds automatic saving of last executed query to localStorage
|
||||
- Adds request middleware to log incoming form params in debug mode
|
||||
|
||||
## 0.6.2 - 2015-07-15
|
||||
|
||||
- Adds ability to specify connection strings prefixed by `postgresql://`, [GH-92]
|
||||
- Updates configuration for Heroku, [GH-89], [GH-90]
|
||||
- Updates postgresql library dependency to latest, [GH-91]
|
||||
- Fixes password field to not display plaintext passwords, [GH-87]
|
||||
|
||||
## 0.6.1 - 2015-06-18
|
||||
|
||||
- This release is repackage-release targeted to fix binary downloads
|
||||
|
||||
## 0.6.0 - 2015-05-31
|
||||
|
||||
- Adds ability to execute only selected SQL query in run command view, [GH-85]
|
||||
- Adds ability to delete/truncate table via context meny on sidebar view
|
||||
- Adds ability to export table contents to CSV via context menu on sidebar view
|
||||
- Changes sidebar color scheme to a lighter and better looking one
|
||||
|
||||
## 0.5.3 - 2015-05-06
|
||||
|
||||
- Changes default server port from 8080 to 8081 to avoil conflict with RethinkDB
|
||||
- Changes styles for table rows and connection settings window
|
||||
- Adds highlighting styles for columns with sort order
|
||||
- Adds git sha into program version output
|
||||
- Add new endpoint /api/info to get build details
|
||||
|
||||
## 0.5.2 - 2015-04-13
|
||||
|
||||
- Adds a new endpoint /activity that retuns active queries
|
||||
- Adds tab to view active queries
|
||||
- Adds column sorting when browsing table contents
|
||||
- Fixes SQL query view when switching to table structure view
|
||||
|
||||
## 0.5.1 - 2015-02-23
|
||||
|
||||
- Upgrades Gin framework dependency to 0.5.0
|
||||
- Fixes server crash if another pgweb server is running
|
||||
|
||||
## 0.5.0 - 2015-01-13
|
||||
|
||||
- Adds Go 1.4 support
|
||||
- Adds connection string printing in debug mode
|
||||
- Adds initial bookmarks support
|
||||
- Adds /api prefix for all API calls
|
||||
- Adds makefile usage task
|
||||
- Adds windows CI to verify build process
|
||||
- Adds example sql database to codebase
|
||||
- Adds timestamped filenames when exporting results to CSV [GH-75]
|
||||
- Adds connection checking on each request to prevent api panics
|
||||
- Adds timestamps to query history records
|
||||
- Adds current database name to the sidebar
|
||||
- Adds button to refresh tables list to the sidebar
|
||||
- Updates all application dependencies
|
||||
- Changes /api/info endpoint to /api/connection
|
||||
- Fixes issues with connection string/options parsing
|
||||
- Fixes capitalized column names in table view
|
||||
- Fixes connection string validation in /api/connect endpoint
|
||||
|
||||
## 0.4.1 - 2014-12-01
|
||||
|
||||
- Adds pgweb version on start [GH-65]
|
||||
- Adds user detection from OS environment
|
||||
- Adds simple memory profiles with --debug option
|
||||
- Adds the session user and search path in connection info [GH-67]
|
||||
- Adds table list reloading after CREATE/DROP TABLE queries [GH-69]
|
||||
- Adds font awesome icons for the sidebar
|
||||
- Removes query recording for internal queries [GH-67]
|
||||
- Fixes default sslmode. Its not longer set to "disable"
|
||||
- Fixes cells cropping on table indexes view
|
||||
- Fixes connection URL generation using web interface
|
||||
- Fixes SQL statements for table row count [GH-67]
|
||||
- Fixes /tables JSON response if database does not have any tables
|
||||
|
||||
## 0.4.0 - 2014-11-11
|
||||
|
||||
- Adds query escaping when exporting results to CSV [GH-38]
|
||||
- Adds keyboard shortcut (ctrl+e, command+e on mac) for query explain action
|
||||
@@ -19,7 +148,7 @@
|
||||
- Fixes sidebar navigation scrolling styles [GH-12]
|
||||
- Fixes sidebar table name styles to support long names
|
||||
|
||||
## 0.3.1
|
||||
## 0.3.1 - 2014-10-28
|
||||
|
||||
- Adds proper exit code when printing version via -v/--version flag
|
||||
- Adds --version and --debug long flag names
|
||||
@@ -29,7 +158,7 @@
|
||||
- Adds --pass flag to specify connection password
|
||||
- Fixes --ssl flag usage, previous value was hardcoded
|
||||
|
||||
## 0.3.0
|
||||
## 0.3.0 - 2014-10-26
|
||||
|
||||
- Renamed `make deps` to `make setup` and fix issues with bootstrapping
|
||||
- Removed hardcoded url for CSV export, it now detects application host:port
|
||||
@@ -37,13 +166,13 @@
|
||||
- Moved table information view to the sidebar
|
||||
- Added --listen flag to specify web server port, still defaults to 8080
|
||||
|
||||
## 0.2.0
|
||||
## 0.2.0 - 2014-10-23
|
||||
|
||||
- Design tweaks
|
||||
- Automatically opens browser on OSX systems
|
||||
- Adds query explain functionality
|
||||
- Adds export to CSV
|
||||
|
||||
## 0.1.0
|
||||
## 0.1.0 - 2014-10-14
|
||||
|
||||
- Initial release
|
||||
@@ -0,0 +1,6 @@
|
||||
- Fork repository
|
||||
- Create a new git branch
|
||||
- Make changes
|
||||
- Run tests: `make test`
|
||||
- If you change frontend code (js/css) make sure to rebuild assets: `make assets`
|
||||
- Open a PR
|
||||
+14
-8
@@ -1,11 +1,17 @@
|
||||
FROM golang:1.3
|
||||
FROM alpine:3.3
|
||||
MAINTAINER Dan Sosedoff <dan.sosedoff@gmail.com>
|
||||
|
||||
COPY . /go/src/pgweb
|
||||
WORKDIR /go/src/pgweb
|
||||
ENV PGWEB_VERSION 0.9.3
|
||||
|
||||
RUN go get github.com/tools/godep
|
||||
RUN \
|
||||
apk update && \
|
||||
apk add ca-certificates && \
|
||||
update-ca-certificates && \
|
||||
cd /tmp && \
|
||||
wget https://github.com/sosedoff/pgweb/releases/download/v$PGWEB_VERSION/pgweb_linux_amd64.zip && \
|
||||
unzip pgweb_linux_amd64.zip -d /usr/bin && \
|
||||
mv /usr/bin/pgweb_linux_amd64 /usr/bin/pgweb && \
|
||||
rm -f pgweb_linux_amd64.zip
|
||||
|
||||
RUN godep restore
|
||||
RUN godep go build && godep go install
|
||||
|
||||
ENTRYPOINT ["pgweb"]
|
||||
EXPOSE 8081
|
||||
CMD ["/usr/bin/pgweb", "--bind=0.0.0.0", "--listen=8081"]
|
||||
Generated
+66
-10
@@ -1,29 +1,85 @@
|
||||
{
|
||||
"ImportPath": "pgweb",
|
||||
"GoVersion": "go1.3.3",
|
||||
"ImportPath": "github.com/sosedoff/pgweb",
|
||||
"GoVersion": "go1.6",
|
||||
"GodepVersion": "v71",
|
||||
"Packages": [
|
||||
"./..."
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/BurntSushi/toml",
|
||||
"Comment": "v0.1.0-9-g3883ac1",
|
||||
"Rev": "3883ac1ce943878302255f538fce319d23226223"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/davecgh/go-spew/spew",
|
||||
"Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gin-gonic/gin",
|
||||
"Comment": "v0.4-8-g3b079bb",
|
||||
"Rev": "3b079bb6f79ae501e73a5401c41b589f6616b61e"
|
||||
"Comment": "v0.5",
|
||||
"Rev": "50e7e427300a7eeeb299a4cd5bc1ebfa04d48528"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gin-gonic/gin/binding",
|
||||
"Comment": "v0.5",
|
||||
"Rev": "50e7e427300a7eeeb299a4cd5bc1ebfa04d48528"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gin-gonic/gin/render",
|
||||
"Comment": "v0.5",
|
||||
"Rev": "50e7e427300a7eeeb299a4cd5bc1ebfa04d48528"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/jessevdk/go-flags",
|
||||
"Comment": "v1-265-g1c87b97",
|
||||
"Rev": "1c87b9727cd7c200be13a5d8280d7ea514469235"
|
||||
"Comment": "v1-285-g1679536",
|
||||
"Rev": "1679536dcc895411a9f5848d9a0250be7856448c"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/jmoiron/sqlx",
|
||||
"Comment": "sqlx-v1.0-49-g79215a1",
|
||||
"Rev": "79215a1536dd456d2a5afa112479b4f67fbe9f21"
|
||||
"Comment": "sqlx-v1.0-61-gb468c08",
|
||||
"Rev": "b468c08552f4efac78b94708eb040170a8184c47"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/jmoiron/sqlx/reflectx",
|
||||
"Comment": "sqlx-v1.0-61-gb468c08",
|
||||
"Rev": "b468c08552f4efac78b94708eb040170a8184c47"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/julienschmidt/httprouter",
|
||||
"Rev": "46807412fe50aaceb73bb57061c2230fd26a1640"
|
||||
"Comment": "v1-37-gb55664b",
|
||||
"Rev": "b55664b9e92004aebb7f19a19a9d06271f3a41fc"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/lib/pq",
|
||||
"Rev": "7175accbed18058468c07811f76440d6e8d7cf19"
|
||||
"Comment": "go1.0-cutoff-56-gdc50b6a",
|
||||
"Rev": "dc50b6ad2d3ee836442cf3389009c7cd1e64bb43"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/lib/pq/oid",
|
||||
"Comment": "go1.0-cutoff-56-gdc50b6a",
|
||||
"Rev": "dc50b6ad2d3ee836442cf3389009c7cd1e64bb43"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/mitchellh/go-homedir",
|
||||
"Rev": "7d2d8c8a4e078ce3c58736ab521a40b37a504c52"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/pmezard/go-difflib/difflib",
|
||||
"Rev": "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/stretchr/testify/assert",
|
||||
"Comment": "v1.1.3",
|
||||
"Rev": "f390dcf405f7b83c997eac1b06768bb9f44dec18"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/curve25519",
|
||||
"Rev": "3760e016850398b85094c4c99e955b8c3dea5711"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/ssh",
|
||||
"Rev": "3760e016850398b85094c4c99e955b8c3dea5711"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generated
-2
@@ -1,2 +0,0 @@
|
||||
/pkg
|
||||
/bin
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"ImportPath": "github.com/gin-gonic/gin",
|
||||
"GoVersion": "go1.3",
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/julienschmidt/httprouter",
|
||||
"Rev": "7deadb6844d2c6ff1dfb812eaa439b87cdaedf20"
|
||||
}
|
||||
]
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBasicAuthSucceed(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/login", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := New()
|
||||
accounts := Accounts{"admin": "password"}
|
||||
r.Use(BasicAuth(accounts))
|
||||
|
||||
r.GET("/login", func(c *Context) {
|
||||
c.String(200, "autorized")
|
||||
})
|
||||
|
||||
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != 200 {
|
||||
t.Errorf("Response code should be Ok, was: %s", w.Code)
|
||||
}
|
||||
bodyAsString := w.Body.String()
|
||||
|
||||
if bodyAsString != "autorized" {
|
||||
t.Errorf("Response body should be `autorized`, was %s", bodyAsString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuth401(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/login", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := New()
|
||||
accounts := Accounts{"foo": "bar"}
|
||||
r.Use(BasicAuth(accounts))
|
||||
|
||||
r.GET("/login", func(c *Context) {
|
||||
c.String(200, "autorized")
|
||||
})
|
||||
|
||||
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != 401 {
|
||||
t.Errorf("Response code should be Not autorized, was: %s", w.Code)
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"Authorization Required\"" {
|
||||
t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
-442
@@ -1,442 +0,0 @@
|
||||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestContextParamsGet tests that a parameter can be parsed from the URL.
|
||||
func TestContextParamsByName(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/test/alexandernyquist", nil)
|
||||
w := httptest.NewRecorder()
|
||||
name := ""
|
||||
|
||||
r := New()
|
||||
r.GET("/test/:name", func(c *Context) {
|
||||
name = c.Params.ByName("name")
|
||||
})
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if name != "alexandernyquist" {
|
||||
t.Errorf("Url parameter was not correctly parsed. Should be alexandernyquist, was %s.", name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestContextSetGet tests that a parameter is set correctly on the
|
||||
// current context and can be retrieved using Get.
|
||||
func TestContextSetGet(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := New()
|
||||
r.GET("/test", func(c *Context) {
|
||||
// Key should be lazily created
|
||||
if c.Keys != nil {
|
||||
t.Error("Keys should be nil")
|
||||
}
|
||||
|
||||
// Set
|
||||
c.Set("foo", "bar")
|
||||
|
||||
v, err := c.Get("foo")
|
||||
if err != nil {
|
||||
t.Errorf("Error on exist key")
|
||||
}
|
||||
if v != "bar" {
|
||||
t.Errorf("Value should be bar, was %s", v)
|
||||
}
|
||||
})
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// TestContextJSON tests that the response is serialized as JSON
|
||||
// and Content-Type is set to application/json
|
||||
func TestContextJSON(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := New()
|
||||
r.GET("/test", func(c *Context) {
|
||||
c.JSON(200, H{"foo": "bar"})
|
||||
})
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Body.String() != "{\"foo\":\"bar\"}\n" {
|
||||
t.Errorf("Response should be {\"foo\":\"bar\"}, was: %s", w.Body.String())
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestContextHTML tests that the response executes the templates
|
||||
// and responds with Content-Type set to text/html
|
||||
func TestContextHTML(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := New()
|
||||
templ, _ := template.New("t").Parse(`Hello {{.Name}}`)
|
||||
r.SetHTMLTemplate(templ)
|
||||
|
||||
type TestData struct{ Name string }
|
||||
|
||||
r.GET("/test", func(c *Context) {
|
||||
c.HTML(200, "t", TestData{"alexandernyquist"})
|
||||
})
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Body.String() != "Hello alexandernyquist" {
|
||||
t.Errorf("Response should be Hello alexandernyquist, was: %s", w.Body.String())
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("Content-Type") != "text/html" {
|
||||
t.Errorf("Content-Type should be text/html, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestContextString tests that the response is returned
|
||||
// with Content-Type set to text/plain
|
||||
func TestContextString(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := New()
|
||||
r.GET("/test", func(c *Context) {
|
||||
c.String(200, "test")
|
||||
})
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Body.String() != "test" {
|
||||
t.Errorf("Response should be test, was: %s", w.Body.String())
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("Content-Type") != "text/plain" {
|
||||
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestContextXML tests that the response is serialized as XML
|
||||
// and Content-Type is set to application/xml
|
||||
func TestContextXML(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := New()
|
||||
r.GET("/test", func(c *Context) {
|
||||
c.XML(200, H{"foo": "bar"})
|
||||
})
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Body.String() != "<map><foo>bar</foo></map>" {
|
||||
t.Errorf("Response should be <map><foo>bar</foo></map>, was: %s", w.Body.String())
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("Content-Type") != "application/xml" {
|
||||
t.Errorf("Content-Type should be application/xml, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestContextData tests that the response can be written from `bytesting`
|
||||
// with specified MIME type
|
||||
func TestContextData(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/test/csv", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := New()
|
||||
r.GET("/test/csv", func(c *Context) {
|
||||
c.Data(200, "text/csv", []byte(`foo,bar`))
|
||||
})
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Body.String() != "foo,bar" {
|
||||
t.Errorf("Response should be foo&bar, was: %s", w.Body.String())
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("Content-Type") != "text/csv" {
|
||||
t.Errorf("Content-Type should be text/csv, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextFile(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/test/file", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := New()
|
||||
r.GET("/test/file", func(c *Context) {
|
||||
c.File("./gin.go")
|
||||
})
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
bodyAsString := w.Body.String()
|
||||
|
||||
if len(bodyAsString) == 0 {
|
||||
t.Errorf("Got empty body instead of file data")
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
|
||||
t.Errorf("Content-Type should be text/plain; charset=utf-8, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandlerFunc - ensure that custom middleware works properly
|
||||
func TestHandlerFunc(t *testing.T) {
|
||||
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := New()
|
||||
var stepsPassed int = 0
|
||||
|
||||
r.Use(func(context *Context) {
|
||||
stepsPassed += 1
|
||||
context.Next()
|
||||
stepsPassed += 1
|
||||
})
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != 404 {
|
||||
t.Errorf("Response code should be Not found, was: %s", w.Code)
|
||||
}
|
||||
|
||||
if stepsPassed != 2 {
|
||||
t.Errorf("Falied to switch context in handler function: %s", stepsPassed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers
|
||||
func TestBadAbortHandlersChain(t *testing.T) {
|
||||
// SETUP
|
||||
var stepsPassed int = 0
|
||||
r := New()
|
||||
r.Use(func(c *Context) {
|
||||
stepsPassed += 1
|
||||
c.Next()
|
||||
stepsPassed += 1
|
||||
// after check and abort
|
||||
c.Abort(409)
|
||||
})
|
||||
r.Use(func(c *Context) {
|
||||
stepsPassed += 1
|
||||
c.Next()
|
||||
stepsPassed += 1
|
||||
c.Abort(403)
|
||||
})
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, "GET", "/")
|
||||
|
||||
// TEST
|
||||
if w.Code != 409 {
|
||||
t.Errorf("Response code should be Forbiden, was: %d", w.Code)
|
||||
}
|
||||
if stepsPassed != 4 {
|
||||
t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order
|
||||
func TestAbortHandlersChain(t *testing.T) {
|
||||
// SETUP
|
||||
var stepsPassed int = 0
|
||||
r := New()
|
||||
r.Use(func(context *Context) {
|
||||
stepsPassed += 1
|
||||
context.Abort(409)
|
||||
})
|
||||
r.Use(func(context *Context) {
|
||||
stepsPassed += 1
|
||||
context.Next()
|
||||
stepsPassed += 1
|
||||
})
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, "GET", "/")
|
||||
|
||||
// TEST
|
||||
if w.Code != 409 {
|
||||
t.Errorf("Response code should be Conflict, was: %d", w.Code)
|
||||
}
|
||||
if stepsPassed != 1 {
|
||||
t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as
|
||||
// as well as Abort
|
||||
func TestFailHandlersChain(t *testing.T) {
|
||||
// SETUP
|
||||
var stepsPassed int = 0
|
||||
r := New()
|
||||
r.Use(func(context *Context) {
|
||||
stepsPassed += 1
|
||||
context.Fail(500, errors.New("foo"))
|
||||
})
|
||||
r.Use(func(context *Context) {
|
||||
stepsPassed += 1
|
||||
context.Next()
|
||||
stepsPassed += 1
|
||||
})
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, "GET", "/")
|
||||
|
||||
// TEST
|
||||
if w.Code != 500 {
|
||||
t.Errorf("Response code should be Server error, was: %d", w.Code)
|
||||
}
|
||||
if stepsPassed != 1 {
|
||||
t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingJSON(t *testing.T) {
|
||||
|
||||
body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}"))
|
||||
|
||||
r := New()
|
||||
r.POST("/binding/json", func(c *Context) {
|
||||
var body struct {
|
||||
Foo string `json:"foo"`
|
||||
}
|
||||
if c.Bind(&body) {
|
||||
c.JSON(200, H{"parsed": body.Foo})
|
||||
}
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("POST", "/binding/json", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != 200 {
|
||||
t.Errorf("Response code should be Ok, was: %s", w.Code)
|
||||
}
|
||||
|
||||
if w.Body.String() != "{\"parsed\":\"bar\"}\n" {
|
||||
t.Errorf("Response should be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingJSONEncoding(t *testing.T) {
|
||||
|
||||
body := bytes.NewBuffer([]byte("{\"foo\":\"嘉\"}"))
|
||||
|
||||
r := New()
|
||||
r.POST("/binding/json", func(c *Context) {
|
||||
var body struct {
|
||||
Foo string `json:"foo"`
|
||||
}
|
||||
if c.Bind(&body) {
|
||||
c.JSON(200, H{"parsed": body.Foo})
|
||||
}
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("POST", "/binding/json", body)
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != 200 {
|
||||
t.Errorf("Response code should be Ok, was: %s", w.Code)
|
||||
}
|
||||
|
||||
if w.Body.String() != "{\"parsed\":\"嘉\"}\n" {
|
||||
t.Errorf("Response should be {\"parsed\":\"嘉\"}, was: %s", w.Body.String())
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingJSONNoContentType(t *testing.T) {
|
||||
|
||||
body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}"))
|
||||
|
||||
r := New()
|
||||
r.POST("/binding/json", func(c *Context) {
|
||||
var body struct {
|
||||
Foo string `json:"foo"`
|
||||
}
|
||||
if c.Bind(&body) {
|
||||
c.JSON(200, H{"parsed": body.Foo})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("POST", "/binding/json", body)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != 400 {
|
||||
t.Errorf("Response code should be Bad request, was: %s", w.Code)
|
||||
}
|
||||
|
||||
if w.Body.String() == "{\"parsed\":\"bar\"}\n" {
|
||||
t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("Content-Type") == "application/json" {
|
||||
t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingJSONMalformed(t *testing.T) {
|
||||
|
||||
body := bytes.NewBuffer([]byte("\"foo\":\"bar\"\n"))
|
||||
|
||||
r := New()
|
||||
r.POST("/binding/json", func(c *Context) {
|
||||
var body struct {
|
||||
Foo string `json:"foo"`
|
||||
}
|
||||
if c.Bind(&body) {
|
||||
c.JSON(200, H{"parsed": body.Foo})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("POST", "/binding/json", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != 400 {
|
||||
t.Errorf("Response code should be Bad request, was: %s", w.Code)
|
||||
}
|
||||
if w.Body.String() == "{\"parsed\":\"bar\"}\n" {
|
||||
t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
|
||||
}
|
||||
|
||||
if w.HeaderMap.Get("Content-Type") == "application/json" {
|
||||
t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
# Guide to run Gin under App Engine LOCAL Development Server
|
||||
|
||||
1. Download, install and setup Go in your computer. (That includes setting your `$GOPATH`.)
|
||||
2. Download SDK for your platform from here: `https://developers.google.com/appengine/downloads?hl=es#Google_App_Engine_SDK_for_Go`
|
||||
3. Download Gin source code using: `$ go get github.com/gin-gonic/gin`
|
||||
4. Navigate to examples folder: `$ cd $GOPATH/src/github.com/gin-gonic/gin/examples/`
|
||||
5. Run it: `$ goapp serve app-engine/`
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
application: hello
|
||||
version: 1
|
||||
runtime: go
|
||||
api_version: go1
|
||||
|
||||
handlers:
|
||||
- url: /.*
|
||||
script: _go_app
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package hello
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// This function's name is a must. App Engine uses it to drive the requests properly.
|
||||
func init() {
|
||||
// Starts a new Gin instance with no middle-ware
|
||||
r := gin.New()
|
||||
|
||||
// Define your handlers
|
||||
r.GET("/", func(c *gin.Context){
|
||||
c.String(200, "Hello World!")
|
||||
})
|
||||
r.GET("/ping", func(c *gin.Context){
|
||||
c.String(200, "pong")
|
||||
})
|
||||
|
||||
// Handle all requests using net/http
|
||||
http.Handle("/", r)
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var DB = make(map[string]string)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// Ping test
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(200, "pong")
|
||||
})
|
||||
|
||||
// Get user value
|
||||
r.GET("/user/:name", func(c *gin.Context) {
|
||||
user := c.Params.ByName("name")
|
||||
value, ok := DB[user]
|
||||
if ok {
|
||||
c.JSON(200, gin.H{"user": user, "value": value})
|
||||
} else {
|
||||
c.JSON(200, gin.H{"user": user, "status": "no value"})
|
||||
}
|
||||
})
|
||||
|
||||
// Authorized group (uses gin.BasicAuth() middleware)
|
||||
// Same than:
|
||||
// authorized := r.Group("/")
|
||||
// authorized.Use(gin.BasicAuth(gin.Credentials{
|
||||
// "foo": "bar",
|
||||
// "manu": "123",
|
||||
//}))
|
||||
authorized := r.Group("/", gin.BasicAuth(gin.Accounts{
|
||||
"foo": "bar", // user:foo password:bar
|
||||
"manu": "123", // user:manu password:123
|
||||
}))
|
||||
|
||||
authorized.POST("admin", func(c *gin.Context) {
|
||||
user := c.MustGet(gin.AuthUserKey).(string)
|
||||
|
||||
// Parse JSON
|
||||
var json struct {
|
||||
Value string `json:"value" binding:"required"`
|
||||
}
|
||||
|
||||
if c.Bind(&json) {
|
||||
DB[user] = json.Value
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
}
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
r.Run(":8080")
|
||||
}
|
||||
-262
@@ -1,262 +0,0 @@
|
||||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"html/template"
|
||||
"math"
|
||||
"net/http"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
AbortIndex = math.MaxInt8 / 2
|
||||
MIMEJSON = "application/json"
|
||||
MIMEHTML = "text/html"
|
||||
MIMEXML = "application/xml"
|
||||
MIMEXML2 = "text/xml"
|
||||
MIMEPlain = "text/plain"
|
||||
MIMEPOSTForm = "application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
type (
|
||||
HandlerFunc func(*Context)
|
||||
|
||||
// Used internally to configure router, a RouterGroup is associated with a prefix
|
||||
// and an array of handlers (middlewares)
|
||||
RouterGroup struct {
|
||||
Handlers []HandlerFunc
|
||||
prefix string
|
||||
parent *RouterGroup
|
||||
engine *Engine
|
||||
}
|
||||
|
||||
// Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares.
|
||||
Engine struct {
|
||||
*RouterGroup
|
||||
HTMLRender render.Render
|
||||
cache sync.Pool
|
||||
finalNoRoute []HandlerFunc
|
||||
noRoute []HandlerFunc
|
||||
router *httprouter.Router
|
||||
}
|
||||
)
|
||||
|
||||
func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) {
|
||||
c := engine.createContext(w, req, nil, engine.finalNoRoute)
|
||||
// set 404 by default, useful for logging
|
||||
c.Writer.WriteHeader(404)
|
||||
c.Next()
|
||||
if !c.Writer.Written() {
|
||||
if c.Writer.Status() == 404 {
|
||||
c.Data(-1, MIMEPlain, []byte("404 page not found"))
|
||||
} else {
|
||||
c.Writer.WriteHeaderNow()
|
||||
}
|
||||
}
|
||||
engine.cache.Put(c)
|
||||
}
|
||||
|
||||
// Returns a new blank Engine instance without any middleware attached.
|
||||
// The most basic configuration
|
||||
func New() *Engine {
|
||||
engine := &Engine{}
|
||||
engine.RouterGroup = &RouterGroup{nil, "/", nil, engine}
|
||||
engine.router = httprouter.New()
|
||||
engine.router.NotFound = engine.handle404
|
||||
engine.cache.New = func() interface{} {
|
||||
c := &Context{Engine: engine}
|
||||
c.Writer = &c.writermem
|
||||
return c
|
||||
}
|
||||
return engine
|
||||
}
|
||||
|
||||
// Returns a Engine instance with the Logger and Recovery already attached.
|
||||
func Default() *Engine {
|
||||
engine := New()
|
||||
engine.Use(Recovery(), Logger())
|
||||
return engine
|
||||
}
|
||||
|
||||
func (engine *Engine) LoadHTMLGlob(pattern string) {
|
||||
if gin_mode == debugCode {
|
||||
engine.HTMLRender = render.HTMLDebug
|
||||
} else {
|
||||
templ := template.Must(template.ParseGlob(pattern))
|
||||
engine.SetHTMLTemplate(templ)
|
||||
}
|
||||
}
|
||||
|
||||
func (engine *Engine) LoadHTMLFiles(files ...string) {
|
||||
if gin_mode == debugCode {
|
||||
engine.HTMLRender = render.HTMLDebug
|
||||
} else {
|
||||
templ := template.Must(template.ParseFiles(files...))
|
||||
engine.SetHTMLTemplate(templ)
|
||||
}
|
||||
}
|
||||
|
||||
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
|
||||
engine.HTMLRender = render.HTMLRender{
|
||||
Template: templ,
|
||||
}
|
||||
}
|
||||
|
||||
// Adds handlers for NoRoute. It return a 404 code by default.
|
||||
func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
|
||||
engine.noRoute = handlers
|
||||
engine.finalNoRoute = engine.combineHandlers(engine.noRoute)
|
||||
}
|
||||
|
||||
func (engine *Engine) Use(middlewares ...HandlerFunc) {
|
||||
engine.RouterGroup.Use(middlewares...)
|
||||
engine.finalNoRoute = engine.combineHandlers(engine.noRoute)
|
||||
}
|
||||
|
||||
// ServeHTTP makes the router implement the http.Handler interface.
|
||||
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
engine.router.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func (engine *Engine) Run(addr string) {
|
||||
if gin_mode == debugCode {
|
||||
fmt.Println("[GIN-debug] Listening and serving HTTP on " + addr)
|
||||
}
|
||||
if err := http.ListenAndServe(addr, engine); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (engine *Engine) RunTLS(addr string, cert string, key string) {
|
||||
if gin_mode == debugCode {
|
||||
fmt.Println("[GIN-debug] Listening and serving HTTPS on " + addr)
|
||||
}
|
||||
if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
/************************************/
|
||||
/********** ROUTES GROUPING *********/
|
||||
/************************************/
|
||||
|
||||
// Adds middlewares to the group, see example code in github.
|
||||
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
|
||||
group.Handlers = append(group.Handlers, middlewares...)
|
||||
}
|
||||
|
||||
// Creates a new router group. You should add all the routes that have common middlwares or the same path prefix.
|
||||
// For example, all the routes that use a common middlware for authorization could be grouped.
|
||||
func (group *RouterGroup) Group(component string, handlers ...HandlerFunc) *RouterGroup {
|
||||
prefix := group.pathFor(component)
|
||||
|
||||
return &RouterGroup{
|
||||
Handlers: group.combineHandlers(handlers),
|
||||
parent: group,
|
||||
prefix: prefix,
|
||||
engine: group.engine,
|
||||
}
|
||||
}
|
||||
|
||||
func (group *RouterGroup) pathFor(p string) string {
|
||||
joined := path.Join(group.prefix, p)
|
||||
// Append a '/' if the last component had one, but only if it's not there already
|
||||
if len(p) > 0 && p[len(p)-1] == '/' && joined[len(joined)-1] != '/' {
|
||||
return joined + "/"
|
||||
}
|
||||
return joined
|
||||
}
|
||||
|
||||
// Handle registers a new request handle and middlewares with the given path and method.
|
||||
// The last handler should be the real handler, the other ones should be middlewares that can and should be shared among different routes.
|
||||
// See the example code in github.
|
||||
//
|
||||
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
|
||||
// functions can be used.
|
||||
//
|
||||
// This function is intended for bulk loading and to allow the usage of less
|
||||
// frequently used, non-standardized or custom methods (e.g. for internal
|
||||
// communication with a proxy).
|
||||
func (group *RouterGroup) Handle(method, p string, handlers []HandlerFunc) {
|
||||
p = group.pathFor(p)
|
||||
handlers = group.combineHandlers(handlers)
|
||||
if gin_mode == debugCode {
|
||||
nuHandlers := len(handlers)
|
||||
name := funcName(handlers[nuHandlers-1])
|
||||
fmt.Printf("[GIN-debug] %-5s %-25s --> %s (%d handlers)\n", method, p, name, nuHandlers)
|
||||
}
|
||||
group.engine.router.Handle(method, p, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
c := group.engine.createContext(w, req, params, handlers)
|
||||
c.Next()
|
||||
c.Writer.WriteHeaderNow()
|
||||
group.engine.cache.Put(c)
|
||||
})
|
||||
}
|
||||
|
||||
// POST is a shortcut for router.Handle("POST", path, handle)
|
||||
func (group *RouterGroup) POST(path string, handlers ...HandlerFunc) {
|
||||
group.Handle("POST", path, handlers)
|
||||
}
|
||||
|
||||
// GET is a shortcut for router.Handle("GET", path, handle)
|
||||
func (group *RouterGroup) GET(path string, handlers ...HandlerFunc) {
|
||||
group.Handle("GET", path, handlers)
|
||||
}
|
||||
|
||||
// DELETE is a shortcut for router.Handle("DELETE", path, handle)
|
||||
func (group *RouterGroup) DELETE(path string, handlers ...HandlerFunc) {
|
||||
group.Handle("DELETE", path, handlers)
|
||||
}
|
||||
|
||||
// PATCH is a shortcut for router.Handle("PATCH", path, handle)
|
||||
func (group *RouterGroup) PATCH(path string, handlers ...HandlerFunc) {
|
||||
group.Handle("PATCH", path, handlers)
|
||||
}
|
||||
|
||||
// PUT is a shortcut for router.Handle("PUT", path, handle)
|
||||
func (group *RouterGroup) PUT(path string, handlers ...HandlerFunc) {
|
||||
group.Handle("PUT", path, handlers)
|
||||
}
|
||||
|
||||
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
|
||||
func (group *RouterGroup) OPTIONS(path string, handlers ...HandlerFunc) {
|
||||
group.Handle("OPTIONS", path, handlers)
|
||||
}
|
||||
|
||||
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
|
||||
func (group *RouterGroup) HEAD(path string, handlers ...HandlerFunc) {
|
||||
group.Handle("HEAD", path, handlers)
|
||||
}
|
||||
|
||||
// Static serves files from the given file system root.
|
||||
// Internally a http.FileServer is used, therefore http.NotFound is used instead
|
||||
// of the Router's NotFound handler.
|
||||
// To use the operating system's file system implementation,
|
||||
// use :
|
||||
// router.Static("/static", "/var/www")
|
||||
func (group *RouterGroup) Static(p, root string) {
|
||||
prefix := group.pathFor(p)
|
||||
p = path.Join(p, "/*filepath")
|
||||
fileServer := http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
|
||||
group.GET(p, func(c *Context) {
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
group.HEAD(p, func(c *Context) {
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
}
|
||||
|
||||
func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc {
|
||||
s := len(group.Handlers) + len(handlers)
|
||||
h := make([]HandlerFunc, 0, s)
|
||||
h = append(h, group.Handlers...)
|
||||
h = append(h, handlers...)
|
||||
return h
|
||||
}
|
||||
-207
@@ -1,207 +0,0 @@
|
||||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SetMode(TestMode)
|
||||
}
|
||||
|
||||
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
||||
req, _ := http.NewRequest(method, path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
// TestSingleRouteOK tests that POST route is correctly invoked.
|
||||
func testRouteOK(method string, t *testing.T) {
|
||||
// SETUP
|
||||
passed := false
|
||||
r := New()
|
||||
r.Handle(method, "/test", []HandlerFunc{func(c *Context) {
|
||||
passed = true
|
||||
}})
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, method, "/test")
|
||||
|
||||
// TEST
|
||||
if passed == false {
|
||||
t.Errorf(method + " route handler was not invoked.")
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Status code should be %v, was %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
func TestRouterGroupRouteOK(t *testing.T) {
|
||||
testRouteOK("POST", t)
|
||||
testRouteOK("DELETE", t)
|
||||
testRouteOK("PATCH", t)
|
||||
testRouteOK("PUT", t)
|
||||
testRouteOK("OPTIONS", t)
|
||||
testRouteOK("HEAD", t)
|
||||
}
|
||||
|
||||
// TestSingleRouteOK tests that POST route is correctly invoked.
|
||||
func testRouteNotOK(method string, t *testing.T) {
|
||||
// SETUP
|
||||
passed := false
|
||||
r := New()
|
||||
r.Handle(method, "/test_2", []HandlerFunc{func(c *Context) {
|
||||
passed = true
|
||||
}})
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, method, "/test")
|
||||
|
||||
// TEST
|
||||
if passed == true {
|
||||
t.Errorf(method + " route handler was invoked, when it should not")
|
||||
}
|
||||
if w.Code != http.StatusNotFound {
|
||||
// If this fails, it's because httprouter needs to be updated to at least f78f58a0db
|
||||
t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSingleRouteOK tests that POST route is correctly invoked.
|
||||
func TestRouteNotOK(t *testing.T) {
|
||||
testRouteNotOK("POST", t)
|
||||
testRouteNotOK("DELETE", t)
|
||||
testRouteNotOK("PATCH", t)
|
||||
testRouteNotOK("PUT", t)
|
||||
testRouteNotOK("OPTIONS", t)
|
||||
testRouteNotOK("HEAD", t)
|
||||
}
|
||||
|
||||
// TestSingleRouteOK tests that POST route is correctly invoked.
|
||||
func testRouteNotOK2(method string, t *testing.T) {
|
||||
// SETUP
|
||||
passed := false
|
||||
r := New()
|
||||
var methodRoute string
|
||||
if method == "POST" {
|
||||
methodRoute = "GET"
|
||||
} else {
|
||||
methodRoute = "POST"
|
||||
}
|
||||
r.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) {
|
||||
passed = true
|
||||
}})
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, method, "/test")
|
||||
|
||||
// TEST
|
||||
if passed == true {
|
||||
t.Errorf(method + " route handler was invoked, when it should not")
|
||||
}
|
||||
if w.Code != http.StatusNotFound {
|
||||
// If this fails, it's because httprouter needs to be updated to at least f78f58a0db
|
||||
t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSingleRouteOK tests that POST route is correctly invoked.
|
||||
func TestRouteNotOK2(t *testing.T) {
|
||||
testRouteNotOK2("POST", t)
|
||||
testRouteNotOK2("DELETE", t)
|
||||
testRouteNotOK2("PATCH", t)
|
||||
testRouteNotOK2("PUT", t)
|
||||
testRouteNotOK2("OPTIONS", t)
|
||||
testRouteNotOK2("HEAD", t)
|
||||
}
|
||||
|
||||
// TestHandleStaticFile - ensure the static file handles properly
|
||||
func TestHandleStaticFile(t *testing.T) {
|
||||
// SETUP file
|
||||
testRoot, _ := os.Getwd()
|
||||
f, err := ioutil.TempFile(testRoot, "")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
filePath := path.Join("/", path.Base(f.Name()))
|
||||
f.WriteString("Gin Web Framework")
|
||||
f.Close()
|
||||
|
||||
// SETUP gin
|
||||
r := New()
|
||||
r.Static("./", testRoot)
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, "GET", filePath)
|
||||
|
||||
// TEST
|
||||
if w.Code != 200 {
|
||||
t.Errorf("Response code should be Ok, was: %s", w.Code)
|
||||
}
|
||||
if w.Body.String() != "Gin Web Framework" {
|
||||
t.Errorf("Response should be test, was: %s", w.Body.String())
|
||||
}
|
||||
if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
|
||||
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleStaticDir - ensure the root/sub dir handles properly
|
||||
func TestHandleStaticDir(t *testing.T) {
|
||||
// SETUP
|
||||
r := New()
|
||||
r.Static("/", "./")
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, "GET", "/")
|
||||
|
||||
// TEST
|
||||
bodyAsString := w.Body.String()
|
||||
if w.Code != 200 {
|
||||
t.Errorf("Response code should be Ok, was: %s", w.Code)
|
||||
}
|
||||
if len(bodyAsString) == 0 {
|
||||
t.Errorf("Got empty body instead of file tree")
|
||||
}
|
||||
if !strings.Contains(bodyAsString, "gin.go") {
|
||||
t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString)
|
||||
}
|
||||
if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
|
||||
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleHeadToDir - ensure the root/sub dir handles properly
|
||||
func TestHandleHeadToDir(t *testing.T) {
|
||||
// SETUP
|
||||
r := New()
|
||||
r.Static("/", "./")
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, "HEAD", "/")
|
||||
|
||||
// TEST
|
||||
bodyAsString := w.Body.String()
|
||||
if w.Code != 200 {
|
||||
t.Errorf("Response code should be Ok, was: %s", w.Code)
|
||||
}
|
||||
if len(bodyAsString) == 0 {
|
||||
t.Errorf("Got empty body instead of file tree")
|
||||
}
|
||||
if !strings.Contains(bodyAsString, "gin.go") {
|
||||
t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString)
|
||||
}
|
||||
if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
|
||||
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ErrorLogger() HandlerFunc {
|
||||
return ErrorLoggerT(ErrorTypeAll)
|
||||
}
|
||||
|
||||
func ErrorLoggerT(typ uint32) HandlerFunc {
|
||||
return func(c *Context) {
|
||||
c.Next()
|
||||
|
||||
errs := c.Errors.ByType(typ)
|
||||
if len(errs) > 0 {
|
||||
// -1 status code = do not change current one
|
||||
c.JSON(-1, c.Errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
|
||||
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
|
||||
yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
|
||||
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
|
||||
reset = string([]byte{27, 91, 48, 109})
|
||||
)
|
||||
|
||||
func Logger() HandlerFunc {
|
||||
stdlogger := log.New(os.Stdout, "", 0)
|
||||
//errlogger := log.New(os.Stderr, "", 0)
|
||||
|
||||
return func(c *Context) {
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// save the IP of the requester
|
||||
requester := c.Request.Header.Get("X-Real-IP")
|
||||
// if the requester-header is empty, check the forwarded-header
|
||||
if len(requester) == 0 {
|
||||
requester = c.Request.Header.Get("X-Forwarded-For")
|
||||
}
|
||||
// if the requester is still empty, use the hard-coded address from the socket
|
||||
if len(requester) == 0 {
|
||||
requester = c.Request.RemoteAddr
|
||||
}
|
||||
|
||||
var color string
|
||||
code := c.Writer.Status()
|
||||
switch {
|
||||
case code >= 200 && code <= 299:
|
||||
color = green
|
||||
case code >= 300 && code <= 399:
|
||||
color = white
|
||||
case code >= 400 && code <= 499:
|
||||
color = yellow
|
||||
default:
|
||||
color = red
|
||||
}
|
||||
end := time.Now()
|
||||
latency := end.Sub(start)
|
||||
stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s %4s %s\n%s",
|
||||
end.Format("2006/01/02 - 15:04:05"),
|
||||
color, code, reset,
|
||||
latency,
|
||||
requester,
|
||||
c.Request.Method, c.Request.URL.Path,
|
||||
c.Errors.String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPanicInHandler assert that panic has been recovered.
|
||||
func TestPanicInHandler(t *testing.T) {
|
||||
// SETUP
|
||||
log.SetOutput(bytes.NewBuffer(nil)) // Disable panic logs for testing
|
||||
r := New()
|
||||
r.Use(Recovery())
|
||||
r.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
})
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, "GET", "/recovery")
|
||||
|
||||
// restore logging
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
if w.Code != 500 {
|
||||
t.Errorf("Response code should be Internal Server Error, was: %s", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPanicWithAbort assert that panic has been recovered even if context.Abort was used.
|
||||
func TestPanicWithAbort(t *testing.T) {
|
||||
// SETUP
|
||||
log.SetOutput(bytes.NewBuffer(nil))
|
||||
r := New()
|
||||
r.Use(Recovery())
|
||||
r.GET("/recovery", func(c *Context) {
|
||||
c.Abort(400)
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
})
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(r, "GET", "/recovery")
|
||||
|
||||
// restore logging
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
// TEST
|
||||
if w.Code != 500 {
|
||||
t.Errorf("Response code should be Bad request, was: %s", w.Code)
|
||||
}
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPositional(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Positional struct {
|
||||
Command int
|
||||
Filename string
|
||||
Rest []string
|
||||
} `positional-args:"yes" required:"yes"`
|
||||
}{}
|
||||
|
||||
p := NewParser(&opts, Default)
|
||||
ret, err := p.ParseArgs([]string{"10", "arg_test.go", "a", "b"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if opts.Positional.Command != 10 {
|
||||
t.Fatalf("Expected opts.Positional.Command to be 10, but got %v", opts.Positional.Command)
|
||||
}
|
||||
|
||||
if opts.Positional.Filename != "arg_test.go" {
|
||||
t.Fatalf("Expected opts.Positional.Filename to be \"arg_test.go\", but got %v", opts.Positional.Filename)
|
||||
}
|
||||
|
||||
assertStringArray(t, opts.Positional.Rest, []string{"a", "b"})
|
||||
assertStringArray(t, ret, []string{})
|
||||
}
|
||||
|
||||
func TestPositionalRequired(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Positional struct {
|
||||
Command int
|
||||
Filename string
|
||||
Rest []string
|
||||
} `positional-args:"yes" required:"yes"`
|
||||
}{}
|
||||
|
||||
p := NewParser(&opts, None)
|
||||
_, err := p.ParseArgs([]string{"10"})
|
||||
|
||||
assertError(t, err, ErrRequired, "the required argument `Filename` was not provided")
|
||||
}
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assertCallerInfo() (string, int) {
|
||||
ptr := make([]uintptr, 15)
|
||||
n := runtime.Callers(1, ptr)
|
||||
|
||||
if n == 0 {
|
||||
return "", 0
|
||||
}
|
||||
|
||||
mef := runtime.FuncForPC(ptr[0])
|
||||
mefile, meline := mef.FileLine(ptr[0])
|
||||
|
||||
for i := 2; i < n; i++ {
|
||||
f := runtime.FuncForPC(ptr[i])
|
||||
file, line := f.FileLine(ptr[i])
|
||||
|
||||
if file != mefile {
|
||||
return file, line
|
||||
}
|
||||
}
|
||||
|
||||
return mefile, meline
|
||||
}
|
||||
|
||||
func assertErrorf(t *testing.T, format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
|
||||
file, line := assertCallerInfo()
|
||||
|
||||
t.Errorf("%s:%d: %s", path.Base(file), line, msg)
|
||||
}
|
||||
|
||||
func assertFatalf(t *testing.T, format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
|
||||
file, line := assertCallerInfo()
|
||||
|
||||
t.Fatalf("%s:%d: %s", path.Base(file), line, msg)
|
||||
}
|
||||
|
||||
func assertString(t *testing.T, a string, b string) {
|
||||
if a != b {
|
||||
assertErrorf(t, "Expected %#v, but got %#v", b, a)
|
||||
}
|
||||
}
|
||||
|
||||
func assertStringArray(t *testing.T, a []string, b []string) {
|
||||
if len(a) != len(b) {
|
||||
assertErrorf(t, "Expected %#v, but got %#v", b, a)
|
||||
return
|
||||
}
|
||||
|
||||
for i, v := range a {
|
||||
if b[i] != v {
|
||||
assertErrorf(t, "Expected %#v, but got %#v", b, a)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertBoolArray(t *testing.T, a []bool, b []bool) {
|
||||
if len(a) != len(b) {
|
||||
assertErrorf(t, "Expected %#v, but got %#v", b, a)
|
||||
return
|
||||
}
|
||||
|
||||
for i, v := range a {
|
||||
if b[i] != v {
|
||||
assertErrorf(t, "Expected %#v, but got %#v", b, a)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertParserSuccess(t *testing.T, data interface{}, args ...string) (*Parser, []string) {
|
||||
parser := NewParser(data, Default&^PrintErrors)
|
||||
ret, err := parser.ParseArgs(args)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected parse error: %s", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return parser, ret
|
||||
}
|
||||
|
||||
func assertParseSuccess(t *testing.T, data interface{}, args ...string) []string {
|
||||
_, ret := assertParserSuccess(t, data, args...)
|
||||
return ret
|
||||
}
|
||||
|
||||
func assertError(t *testing.T, err error, typ ErrorType, msg string) {
|
||||
if err == nil {
|
||||
assertFatalf(t, "Expected error: %s", msg)
|
||||
return
|
||||
}
|
||||
|
||||
if e, ok := err.(*Error); !ok {
|
||||
assertFatalf(t, "Expected Error type, but got %#v", err)
|
||||
} else {
|
||||
if e.Type != typ {
|
||||
assertErrorf(t, "Expected error type {%s}, but got {%s}", typ, e.Type)
|
||||
}
|
||||
|
||||
if e.Message != msg {
|
||||
assertErrorf(t, "Expected error message %#v, but got %#v", msg, e.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertParseFail(t *testing.T, typ ErrorType, msg string, data interface{}, args ...string) []string {
|
||||
parser := NewParser(data, Default&^PrintErrors)
|
||||
ret, err := parser.ParseArgs(args)
|
||||
|
||||
assertError(t, err, typ, msg)
|
||||
return ret
|
||||
}
|
||||
|
||||
func diff(a, b string) (string, error) {
|
||||
atmp, err := ioutil.TempFile("", "help-diff")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
btmp, err := ioutil.TempFile("", "help-diff")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(atmp, a); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(btmp, b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ret, err := exec.Command("diff", "-u", "-d", "--label", "got", atmp.Name(), "--label", "expected", btmp.Name()).Output()
|
||||
|
||||
os.Remove(atmp.Name())
|
||||
os.Remove(btmp.Name())
|
||||
|
||||
if err.Error() == "exit status 1" {
|
||||
return string(ret), nil
|
||||
}
|
||||
|
||||
return string(ret), err
|
||||
}
|
||||
|
||||
func assertDiff(t *testing.T, actual, expected, msg string) {
|
||||
if actual == expected {
|
||||
return
|
||||
}
|
||||
|
||||
ret, err := diff(actual, expected)
|
||||
|
||||
if err != nil {
|
||||
assertErrorf(t, "Unexpected diff error: %s", err)
|
||||
assertErrorf(t, "Unexpected %s, expected:\n\n%s\n\nbut got\n\n%s", msg, expected, actual)
|
||||
} else {
|
||||
assertErrorf(t, "Unexpected %s:\n\n%s", msg, ret)
|
||||
}
|
||||
}
|
||||
-354
@@ -1,354 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommandInline(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Command struct {
|
||||
G bool `short:"g"`
|
||||
} `command:"cmd"`
|
||||
}{}
|
||||
|
||||
p, ret := assertParserSuccess(t, &opts, "-v", "cmd", "-g")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if p.Active == nil {
|
||||
t.Errorf("Expected active command")
|
||||
}
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
|
||||
if !opts.Command.G {
|
||||
t.Errorf("Expected Command.G to be true")
|
||||
}
|
||||
|
||||
if p.Command.Find("cmd") != p.Active {
|
||||
t.Errorf("Expected to find command `cmd' to be active")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandInlineMulti(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
C1 struct {
|
||||
} `command:"c1"`
|
||||
|
||||
C2 struct {
|
||||
G bool `short:"g"`
|
||||
} `command:"c2"`
|
||||
}{}
|
||||
|
||||
p, ret := assertParserSuccess(t, &opts, "-v", "c2", "-g")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if p.Active == nil {
|
||||
t.Errorf("Expected active command")
|
||||
}
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
|
||||
if !opts.C2.G {
|
||||
t.Errorf("Expected C2.G to be true")
|
||||
}
|
||||
|
||||
if p.Command.Find("c1") == nil {
|
||||
t.Errorf("Expected to find command `c1'")
|
||||
}
|
||||
|
||||
if c2 := p.Command.Find("c2"); c2 == nil {
|
||||
t.Errorf("Expected to find command `c2'")
|
||||
} else if c2 != p.Active {
|
||||
t.Errorf("Expected to find command `c2' to be active")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandFlagOrder1(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Command struct {
|
||||
G bool `short:"g"`
|
||||
} `command:"cmd"`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrUnknownFlag, "unknown flag `g'", &opts, "-v", "-g", "cmd")
|
||||
}
|
||||
|
||||
func TestCommandFlagOrder2(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Command struct {
|
||||
G bool `short:"g"`
|
||||
} `command:"cmd"`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrUnknownFlag, "unknown flag `v'", &opts, "cmd", "-v", "-g")
|
||||
}
|
||||
|
||||
func TestCommandEstimate(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Cmd1 struct {
|
||||
} `command:"remove"`
|
||||
|
||||
Cmd2 struct {
|
||||
} `command:"add"`
|
||||
}{}
|
||||
|
||||
p := NewParser(&opts, None)
|
||||
_, err := p.ParseArgs([]string{})
|
||||
|
||||
assertError(t, err, ErrCommandRequired, "Please specify one command of: add or remove")
|
||||
}
|
||||
|
||||
func TestCommandEstimate2(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Cmd1 struct {
|
||||
} `command:"remove"`
|
||||
|
||||
Cmd2 struct {
|
||||
} `command:"add"`
|
||||
}{}
|
||||
|
||||
p := NewParser(&opts, None)
|
||||
_, err := p.ParseArgs([]string{"rmive"})
|
||||
|
||||
assertError(t, err, ErrUnknownCommand, "Unknown command `rmive', did you mean `remove'?")
|
||||
}
|
||||
|
||||
type testCommand struct {
|
||||
G bool `short:"g"`
|
||||
Executed bool
|
||||
EArgs []string
|
||||
}
|
||||
|
||||
func (c *testCommand) Execute(args []string) error {
|
||||
c.Executed = true
|
||||
c.EArgs = args
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCommandExecute(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Command testCommand `command:"cmd"`
|
||||
}{}
|
||||
|
||||
assertParseSuccess(t, &opts, "-v", "cmd", "-g", "a", "b")
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
|
||||
if !opts.Command.Executed {
|
||||
t.Errorf("Did not execute command")
|
||||
}
|
||||
|
||||
if !opts.Command.G {
|
||||
t.Errorf("Expected Command.C to be true")
|
||||
}
|
||||
|
||||
assertStringArray(t, opts.Command.EArgs, []string{"a", "b"})
|
||||
}
|
||||
|
||||
func TestCommandClosest(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Cmd1 struct {
|
||||
} `command:"remove"`
|
||||
|
||||
Cmd2 struct {
|
||||
} `command:"add"`
|
||||
}{}
|
||||
|
||||
args := assertParseFail(t, ErrUnknownCommand, "Unknown command `addd', did you mean `add'?", &opts, "-v", "addd")
|
||||
|
||||
assertStringArray(t, args, []string{"addd"})
|
||||
}
|
||||
|
||||
func TestCommandAdd(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
}{}
|
||||
|
||||
var cmd = struct {
|
||||
G bool `short:"g"`
|
||||
}{}
|
||||
|
||||
p := NewParser(&opts, Default)
|
||||
c, err := p.AddCommand("cmd", "", "", &cmd)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ret, err := p.ParseArgs([]string{"-v", "cmd", "-g", "rest"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
assertStringArray(t, ret, []string{"rest"})
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
|
||||
if !cmd.G {
|
||||
t.Errorf("Expected Command.G to be true")
|
||||
}
|
||||
|
||||
if p.Command.Find("cmd") != c {
|
||||
t.Errorf("Expected to find command `cmd'")
|
||||
}
|
||||
|
||||
if p.Commands()[0] != c {
|
||||
t.Errorf("Expected command %#v, but got %#v", c, p.Commands()[0])
|
||||
}
|
||||
|
||||
if c.Options()[0].ShortName != 'g' {
|
||||
t.Errorf("Expected short name `g' but got %v", c.Options()[0].ShortName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandNestedInline(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Command struct {
|
||||
G bool `short:"g"`
|
||||
|
||||
Nested struct {
|
||||
N string `long:"n"`
|
||||
} `command:"nested"`
|
||||
} `command:"cmd"`
|
||||
}{}
|
||||
|
||||
p, ret := assertParserSuccess(t, &opts, "-v", "cmd", "-g", "nested", "--n", "n", "rest")
|
||||
|
||||
assertStringArray(t, ret, []string{"rest"})
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
|
||||
if !opts.Command.G {
|
||||
t.Errorf("Expected Command.G to be true")
|
||||
}
|
||||
|
||||
assertString(t, opts.Command.Nested.N, "n")
|
||||
|
||||
if c := p.Command.Find("cmd"); c == nil {
|
||||
t.Errorf("Expected to find command `cmd'")
|
||||
} else {
|
||||
if c != p.Active {
|
||||
t.Errorf("Expected `cmd' to be the active parser command")
|
||||
}
|
||||
|
||||
if nested := c.Find("nested"); nested == nil {
|
||||
t.Errorf("Expected to find command `nested'")
|
||||
} else if nested != c.Active {
|
||||
t.Errorf("Expected to find command `nested' to be the active `cmd' command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredOnCommand(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v" required:"true"`
|
||||
|
||||
Command struct {
|
||||
G bool `short:"g"`
|
||||
} `command:"cmd"`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrRequired, fmt.Sprintf("the required flag `%cv' was not specified", defaultShortOptDelimiter), &opts, "cmd")
|
||||
}
|
||||
|
||||
func TestRequiredAllOnCommand(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v" required:"true"`
|
||||
Missing bool `long:"missing" required:"true"`
|
||||
|
||||
Command struct {
|
||||
G bool `short:"g"`
|
||||
} `command:"cmd"`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrRequired, fmt.Sprintf("the required flags `%smissing' and `%cv' were not specified", defaultLongOptDelimiter, defaultShortOptDelimiter), &opts, "cmd")
|
||||
}
|
||||
|
||||
func TestDefaultOnCommand(t *testing.T) {
|
||||
var opts = struct {
|
||||
Command struct {
|
||||
G bool `short:"g" default:"true"`
|
||||
} `command:"cmd"`
|
||||
}{}
|
||||
|
||||
assertParseSuccess(t, &opts, "cmd")
|
||||
|
||||
if !opts.Command.G {
|
||||
t.Errorf("Expected G to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubcommandsOptional(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Cmd1 struct {
|
||||
} `command:"remove"`
|
||||
|
||||
Cmd2 struct {
|
||||
} `command:"add"`
|
||||
}{}
|
||||
|
||||
p := NewParser(&opts, None)
|
||||
p.SubcommandsOptional = true
|
||||
|
||||
_, err := p.ParseArgs([]string{"-v"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandAlias(t *testing.T) {
|
||||
var opts = struct {
|
||||
Command struct {
|
||||
G bool `short:"g" default:"true"`
|
||||
} `command:"cmd" alias:"cm"`
|
||||
}{}
|
||||
|
||||
assertParseSuccess(t, &opts, "cm")
|
||||
|
||||
if !opts.Command.G {
|
||||
t.Errorf("Expected G to be true")
|
||||
}
|
||||
}
|
||||
-289
@@ -1,289 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestComplete struct {
|
||||
}
|
||||
|
||||
func (t *TestComplete) Complete(match string) []Completion {
|
||||
options := []string{
|
||||
"hello world",
|
||||
"hello universe",
|
||||
"hello multiverse",
|
||||
}
|
||||
|
||||
ret := make([]Completion, 0, len(options))
|
||||
|
||||
for _, o := range options {
|
||||
if strings.HasPrefix(o, match) {
|
||||
ret = append(ret, Completion{
|
||||
Item: o,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
var completionTestOptions struct {
|
||||
Verbose bool `short:"v" long:"verbose" description:"Verbose messages"`
|
||||
Debug bool `short:"d" long:"debug" description:"Enable debug"`
|
||||
Version bool `long:"version" description:"Show version"`
|
||||
Required bool `long:"required" required:"true" description:"This is required"`
|
||||
|
||||
AddCommand struct {
|
||||
Positional struct {
|
||||
Filename Filename
|
||||
} `positional-args:"yes"`
|
||||
} `command:"add" description:"add an item"`
|
||||
|
||||
AddMultiCommand struct {
|
||||
Positional struct {
|
||||
Filename []Filename
|
||||
} `positional-args:"yes"`
|
||||
} `command:"add-multi" description:"add multiple items"`
|
||||
|
||||
RemoveCommand struct {
|
||||
Other bool `short:"o"`
|
||||
File Filename `short:"f" long:"filename"`
|
||||
} `command:"rm" description:"remove an item"`
|
||||
|
||||
RenameCommand struct {
|
||||
Completed TestComplete `short:"c" long:"completed"`
|
||||
} `command:"rename" description:"rename an item"`
|
||||
}
|
||||
|
||||
type completionTest struct {
|
||||
Args []string
|
||||
Completed []string
|
||||
ShowDescriptions bool
|
||||
}
|
||||
|
||||
var completionTests []completionTest
|
||||
|
||||
func init() {
|
||||
_, sourcefile, _, _ := runtime.Caller(0)
|
||||
completionTestSourcedir := filepath.Join(filepath.SplitList(path.Dir(sourcefile))...)
|
||||
|
||||
completionTestFilename := []string{filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion_test.go")}
|
||||
|
||||
completionTests = []completionTest{
|
||||
{
|
||||
// Short names
|
||||
[]string{"-"},
|
||||
[]string{"-d", "-v"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Short names concatenated
|
||||
[]string{"-dv"},
|
||||
[]string{"-dv"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Long names
|
||||
[]string{"--"},
|
||||
[]string{"--debug", "--required", "--verbose", "--version"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Long names with descriptions
|
||||
[]string{"--"},
|
||||
[]string{
|
||||
"--debug # Enable debug",
|
||||
"--required # This is required",
|
||||
"--verbose # Verbose messages",
|
||||
"--version # Show version",
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
// Long names partial
|
||||
[]string{"--ver"},
|
||||
[]string{"--verbose", "--version"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Commands
|
||||
[]string{""},
|
||||
[]string{"add", "add-multi", "rename", "rm"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Commands with descriptions
|
||||
[]string{""},
|
||||
[]string{
|
||||
"add # add an item",
|
||||
"add-multi # add multiple items",
|
||||
"rename # rename an item",
|
||||
"rm # remove an item",
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
// Commands partial
|
||||
[]string{"r"},
|
||||
[]string{"rename", "rm"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Positional filename
|
||||
[]string{"add", filepath.Join(completionTestSourcedir, "completion")},
|
||||
completionTestFilename,
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Multiple positional filename (1 arg)
|
||||
[]string{"add-multi", filepath.Join(completionTestSourcedir, "completion")},
|
||||
completionTestFilename,
|
||||
false,
|
||||
},
|
||||
{
|
||||
// Multiple positional filename (2 args)
|
||||
[]string{"add-multi", filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion")},
|
||||
completionTestFilename,
|
||||
false,
|
||||
},
|
||||
{
|
||||
// Multiple positional filename (3 args)
|
||||
[]string{"add-multi", filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion")},
|
||||
completionTestFilename,
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Flag filename
|
||||
[]string{"rm", "-f", path.Join(completionTestSourcedir, "completion")},
|
||||
completionTestFilename,
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Flag short concat last filename
|
||||
[]string{"rm", "-of", path.Join(completionTestSourcedir, "completion")},
|
||||
completionTestFilename,
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Flag concat filename
|
||||
[]string{"rm", "-f" + path.Join(completionTestSourcedir, "completion")},
|
||||
[]string{"-f" + completionTestFilename[0], "-f" + completionTestFilename[1]},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Flag equal concat filename
|
||||
[]string{"rm", "-f=" + path.Join(completionTestSourcedir, "completion")},
|
||||
[]string{"-f=" + completionTestFilename[0], "-f=" + completionTestFilename[1]},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Flag concat long filename
|
||||
[]string{"rm", "--filename=" + path.Join(completionTestSourcedir, "completion")},
|
||||
[]string{"--filename=" + completionTestFilename[0], "--filename=" + completionTestFilename[1]},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Flag long filename
|
||||
[]string{"rm", "--filename", path.Join(completionTestSourcedir, "completion")},
|
||||
completionTestFilename,
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// Custom completed
|
||||
[]string{"rename", "-c", "hello un"},
|
||||
[]string{"hello universe"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompletion(t *testing.T) {
|
||||
p := NewParser(&completionTestOptions, Default)
|
||||
c := &completion{parser: p}
|
||||
|
||||
for _, test := range completionTests {
|
||||
if test.ShowDescriptions {
|
||||
continue
|
||||
}
|
||||
|
||||
ret := c.complete(test.Args)
|
||||
items := make([]string, len(ret))
|
||||
|
||||
for i, v := range ret {
|
||||
items[i] = v.Item
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(items, test.Completed) {
|
||||
t.Errorf("Args: %#v, %#v\n Expected: %#v\n Got: %#v", test.Args, test.ShowDescriptions, test.Completed, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserCompletion(t *testing.T) {
|
||||
for _, test := range completionTests {
|
||||
if test.ShowDescriptions {
|
||||
os.Setenv("GO_FLAGS_COMPLETION", "verbose")
|
||||
} else {
|
||||
os.Setenv("GO_FLAGS_COMPLETION", "1")
|
||||
}
|
||||
|
||||
tmp := os.Stdout
|
||||
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
out := make(chan string)
|
||||
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
|
||||
io.Copy(&buf, r)
|
||||
|
||||
out <- buf.String()
|
||||
}()
|
||||
|
||||
p := NewParser(&completionTestOptions, None)
|
||||
|
||||
_, err := p.ParseArgs(test.Args)
|
||||
|
||||
w.Close()
|
||||
|
||||
os.Stdout = tmp
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
got := strings.Split(strings.Trim(<-out, "\n"), "\n")
|
||||
|
||||
if !reflect.DeepEqual(got, test.Completed) {
|
||||
t.Errorf("Expected: %#v\nGot: %#v", test.Completed, got)
|
||||
}
|
||||
}
|
||||
|
||||
os.Setenv("GO_FLAGS_COMPLETION", "")
|
||||
}
|
||||
-175
@@ -1,175 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func expectConvert(t *testing.T, o *Option, expected string) {
|
||||
s, err := convertToString(o.value, o.tag)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
assertString(t, s, expected)
|
||||
}
|
||||
|
||||
func TestConvertToString(t *testing.T) {
|
||||
d, _ := time.ParseDuration("1h2m4s")
|
||||
|
||||
var opts = struct {
|
||||
String string `long:"string"`
|
||||
|
||||
Int int `long:"int"`
|
||||
Int8 int8 `long:"int8"`
|
||||
Int16 int16 `long:"int16"`
|
||||
Int32 int32 `long:"int32"`
|
||||
Int64 int64 `long:"int64"`
|
||||
|
||||
Uint uint `long:"uint"`
|
||||
Uint8 uint8 `long:"uint8"`
|
||||
Uint16 uint16 `long:"uint16"`
|
||||
Uint32 uint32 `long:"uint32"`
|
||||
Uint64 uint64 `long:"uint64"`
|
||||
|
||||
Float32 float32 `long:"float32"`
|
||||
Float64 float64 `long:"float64"`
|
||||
|
||||
Duration time.Duration `long:"duration"`
|
||||
|
||||
Bool bool `long:"bool"`
|
||||
|
||||
IntSlice []int `long:"int-slice"`
|
||||
IntFloatMap map[int]float64 `long:"int-float-map"`
|
||||
|
||||
PtrBool *bool `long:"ptr-bool"`
|
||||
Interface interface{} `long:"interface"`
|
||||
|
||||
Int32Base int32 `long:"int32-base" base:"16"`
|
||||
Uint32Base uint32 `long:"uint32-base" base:"16"`
|
||||
}{
|
||||
"string",
|
||||
|
||||
-2,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
|
||||
1.2,
|
||||
-3.4,
|
||||
|
||||
d,
|
||||
true,
|
||||
|
||||
[]int{-3, 4, -2},
|
||||
map[int]float64{-2: 4.5},
|
||||
|
||||
new(bool),
|
||||
float32(5.2),
|
||||
|
||||
-5823,
|
||||
4232,
|
||||
}
|
||||
|
||||
p := NewNamedParser("test", Default)
|
||||
grp, _ := p.AddGroup("test group", "", &opts)
|
||||
|
||||
expects := []string{
|
||||
"string",
|
||||
"-2",
|
||||
"-1",
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
|
||||
"1.2",
|
||||
"-3.4",
|
||||
|
||||
"1h2m4s",
|
||||
"true",
|
||||
|
||||
"[-3, 4, -2]",
|
||||
"{-2:4.5}",
|
||||
|
||||
"false",
|
||||
"5.2",
|
||||
|
||||
"-16bf",
|
||||
"1088",
|
||||
}
|
||||
|
||||
for i, v := range grp.Options() {
|
||||
expectConvert(t, v, expects[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToStringInvalidIntBase(t *testing.T) {
|
||||
var opts = struct {
|
||||
Int int `long:"int" base:"no"`
|
||||
}{
|
||||
2,
|
||||
}
|
||||
|
||||
p := NewNamedParser("test", Default)
|
||||
grp, _ := p.AddGroup("test group", "", &opts)
|
||||
o := grp.Options()[0]
|
||||
|
||||
_, err := convertToString(o.value, o.tag)
|
||||
|
||||
if err != nil {
|
||||
err = newErrorf(ErrMarshal, "%v", err)
|
||||
}
|
||||
|
||||
assertError(t, err, ErrMarshal, "strconv.ParseInt: parsing \"no\": invalid syntax")
|
||||
}
|
||||
|
||||
func TestConvertToStringInvalidUintBase(t *testing.T) {
|
||||
var opts = struct {
|
||||
Uint uint `long:"uint" base:"no"`
|
||||
}{
|
||||
2,
|
||||
}
|
||||
|
||||
p := NewNamedParser("test", Default)
|
||||
grp, _ := p.AddGroup("test group", "", &opts)
|
||||
o := grp.Options()[0]
|
||||
|
||||
_, err := convertToString(o.value, o.tag)
|
||||
|
||||
if err != nil {
|
||||
err = newErrorf(ErrMarshal, "%v", err)
|
||||
}
|
||||
|
||||
assertError(t, err, ErrMarshal, "strconv.ParseInt: parsing \"no\": invalid syntax")
|
||||
}
|
||||
|
||||
func TestWrapText(t *testing.T) {
|
||||
s := "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
|
||||
got := wrapText(s, 60, " ")
|
||||
expected := `Lorem ipsum dolor sit amet, consectetur adipisicing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
|
||||
ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
Duis aute irure dolor in reprehenderit in voluptate velit
|
||||
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
|
||||
occaecat cupidatat non proident, sunt in culpa qui officia
|
||||
deserunt mollit anim id est laborum.`
|
||||
|
||||
assertDiff(t, got, expected, "wrapped text")
|
||||
}
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
// Example of use of the flags package.
|
||||
package flags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
var opts struct {
|
||||
// Slice of bool will append 'true' each time the option
|
||||
// is encountered (can be set multiple times, like -vvv)
|
||||
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"`
|
||||
|
||||
// Example of automatic marshalling to desired type (uint)
|
||||
Offset uint `long:"offset" description:"Offset"`
|
||||
|
||||
// Example of a callback, called each time the option is found.
|
||||
Call func(string) `short:"c" description:"Call phone number"`
|
||||
|
||||
// Example of a required flag
|
||||
Name string `short:"n" long:"name" description:"A name" required:"true"`
|
||||
|
||||
// Example of a value name
|
||||
File string `short:"f" long:"file" description:"A file" value-name:"FILE"`
|
||||
|
||||
// Example of a pointer
|
||||
Ptr *int `short:"p" description:"A pointer to an integer"`
|
||||
|
||||
// Example of a slice of strings
|
||||
StringSlice []string `short:"s" description:"A slice of strings"`
|
||||
|
||||
// Example of a slice of pointers
|
||||
PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"`
|
||||
|
||||
// Example of a map
|
||||
IntMap map[string]int `long:"intmap" description:"A map from string to int"`
|
||||
|
||||
// Example of a filename (useful for completion)
|
||||
Filename Filename `long:"filename" description:"A filename"`
|
||||
|
||||
// Example of positional arguments
|
||||
Args struct {
|
||||
Id string
|
||||
Num int
|
||||
Rest []string
|
||||
} `positional-args:"yes" required:"yes"`
|
||||
}
|
||||
|
||||
// Callback which will invoke callto:<argument> to call a number.
|
||||
// Note that this works just on OS X (and probably only with
|
||||
// Skype) but it shows the idea.
|
||||
opts.Call = func(num string) {
|
||||
cmd := exec.Command("open", "callto:"+num)
|
||||
cmd.Start()
|
||||
cmd.Process.Release()
|
||||
}
|
||||
|
||||
// Make some fake arguments to parse.
|
||||
args := []string{
|
||||
"-vv",
|
||||
"--offset=5",
|
||||
"-n", "Me",
|
||||
"-p", "3",
|
||||
"-s", "hello",
|
||||
"-s", "world",
|
||||
"--ptrslice", "hello",
|
||||
"--ptrslice", "world",
|
||||
"--intmap", "a:1",
|
||||
"--intmap", "b:5",
|
||||
"--filename", "hello.go",
|
||||
"id",
|
||||
"10",
|
||||
"remaining1",
|
||||
"remaining2",
|
||||
}
|
||||
|
||||
// Parse flags from `args'. Note that here we use flags.ParseArgs for
|
||||
// the sake of making a working example. Normally, you would simply use
|
||||
// flags.Parse(&opts) which uses os.Args
|
||||
_, err := ParseArgs(&opts, args)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Verbosity: %v\n", opts.Verbose)
|
||||
fmt.Printf("Offset: %d\n", opts.Offset)
|
||||
fmt.Printf("Name: %s\n", opts.Name)
|
||||
fmt.Printf("Ptr: %d\n", *opts.Ptr)
|
||||
fmt.Printf("StringSlice: %v\n", opts.StringSlice)
|
||||
fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1])
|
||||
fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"])
|
||||
fmt.Printf("Filename: %v\n", opts.Filename)
|
||||
fmt.Printf("Args.Id: %s\n", opts.Args.Id)
|
||||
fmt.Printf("Args.Num: %d\n", opts.Args.Num)
|
||||
fmt.Printf("Args.Rest: %v\n", opts.Args.Rest)
|
||||
|
||||
// Output: Verbosity: [true true]
|
||||
// Offset: 5
|
||||
// Name: Me
|
||||
// Ptr: 3
|
||||
// StringSlice: [hello world]
|
||||
// PtrSlice: [hello world]
|
||||
// IntMap: [a:1 b:5]
|
||||
// Filename: hello.go
|
||||
// Args.Id: id
|
||||
// Args.Num: 10
|
||||
// Args.Rest: [remaining1 remaining2]
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type AddCommand struct {
|
||||
All bool `short:"a" long:"all" description:"Add all files"`
|
||||
}
|
||||
|
||||
var addCommand AddCommand
|
||||
|
||||
func (x *AddCommand) Execute(args []string) error {
|
||||
fmt.Printf("Adding (all=%v): %#v\n", x.All, args)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
parser.AddCommand("add",
|
||||
"Add a file",
|
||||
"The add command adds a file to the repository. Use -a to add all files.",
|
||||
&addCommand)
|
||||
}
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
_examples() {
|
||||
args=("${COMP_WORDS[@]:1:$COMP_CWORD}")
|
||||
|
||||
local IFS=$'\n'
|
||||
COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}"))
|
||||
return 1
|
||||
}
|
||||
|
||||
complete -F _examples examples
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EditorOptions struct {
|
||||
Input flags.Filename `short:"i" long:"input" description:"Input file" default:"-"`
|
||||
Output flags.Filename `short:"o" long:"output" description:"Output file" default:"-"`
|
||||
}
|
||||
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
func (p *Point) UnmarshalFlag(value string) error {
|
||||
parts := strings.Split(value, ",")
|
||||
|
||||
if len(parts) != 2 {
|
||||
return errors.New("expected two numbers separated by a ,")
|
||||
}
|
||||
|
||||
x, err := strconv.ParseInt(parts[0], 10, 32)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
y, err := strconv.ParseInt(parts[1], 10, 32)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.X = int(x)
|
||||
p.Y = int(y)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Point) MarshalFlag() (string, error) {
|
||||
return fmt.Sprintf("%d,%d", p.X, p.Y), nil
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
// Example of verbosity with level
|
||||
Verbose []bool `short:"v" long:"verbose" description:"Verbose output"`
|
||||
|
||||
// Example of optional value
|
||||
User string `short:"u" long:"user" description:"User name" optional:"yes" optional-value:"pancake"`
|
||||
|
||||
// Example of map with multiple default values
|
||||
Users map[string]string `long:"users" description:"User e-mail map" default:"system:system@example.org" default:"admin:admin@example.org"`
|
||||
|
||||
// Example of option group
|
||||
Editor EditorOptions `group:"Editor Options"`
|
||||
|
||||
// Example of custom type Marshal/Unmarshal
|
||||
Point Point `long:"point" description:"A x,y point" default:"1,2"`
|
||||
}
|
||||
|
||||
var options Options
|
||||
|
||||
var parser = flags.NewParser(&options, flags.Default)
|
||||
|
||||
func main() {
|
||||
if _, err := parser.Parse(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type RmCommand struct {
|
||||
Force bool `short:"f" long:"force" description:"Force removal of files"`
|
||||
}
|
||||
|
||||
var rmCommand RmCommand
|
||||
|
||||
func (x *RmCommand) Execute(args []string) error {
|
||||
fmt.Printf("Removing (force=%v): %#v\n", x.Force, args)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
parser.AddCommand("rm",
|
||||
"Remove a file",
|
||||
"The rm command removes a file to the repository. Use -f to force removal of files.",
|
||||
&rmCommand)
|
||||
}
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGroupInline(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Group struct {
|
||||
G bool `short:"g"`
|
||||
} `group:"Grouped Options"`
|
||||
}{}
|
||||
|
||||
p, ret := assertParserSuccess(t, &opts, "-v", "-g")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
|
||||
if !opts.Group.G {
|
||||
t.Errorf("Expected Group.G to be true")
|
||||
}
|
||||
|
||||
if p.Command.Group.Find("Grouped Options") == nil {
|
||||
t.Errorf("Expected to find group `Grouped Options'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupAdd(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
}{}
|
||||
|
||||
var grp = struct {
|
||||
G bool `short:"g"`
|
||||
}{}
|
||||
|
||||
p := NewParser(&opts, Default)
|
||||
g, err := p.AddGroup("Grouped Options", "", &grp)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ret, err := p.ParseArgs([]string{"-v", "-g", "rest"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
assertStringArray(t, ret, []string{"rest"})
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
|
||||
if !grp.G {
|
||||
t.Errorf("Expected Group.G to be true")
|
||||
}
|
||||
|
||||
if p.Command.Group.Find("Grouped Options") != g {
|
||||
t.Errorf("Expected to find group `Grouped Options'")
|
||||
}
|
||||
|
||||
if p.Groups()[1] != g {
|
||||
t.Errorf("Expected group %#v, but got %#v", g, p.Groups()[0])
|
||||
}
|
||||
|
||||
if g.Options()[0].ShortName != 'g' {
|
||||
t.Errorf("Expected short name `g' but got %v", g.Options()[0].ShortName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupNestedInline(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
|
||||
Group struct {
|
||||
G bool `short:"g"`
|
||||
|
||||
Nested struct {
|
||||
N string `long:"n"`
|
||||
} `group:"Nested Options"`
|
||||
} `group:"Grouped Options"`
|
||||
}{}
|
||||
|
||||
p, ret := assertParserSuccess(t, &opts, "-v", "-g", "--n", "n", "rest")
|
||||
|
||||
assertStringArray(t, ret, []string{"rest"})
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
|
||||
if !opts.Group.G {
|
||||
t.Errorf("Expected Group.G to be true")
|
||||
}
|
||||
|
||||
assertString(t, opts.Group.Nested.N, "n")
|
||||
|
||||
if p.Command.Group.Find("Grouped Options") == nil {
|
||||
t.Errorf("Expected to find group `Grouped Options'")
|
||||
}
|
||||
|
||||
if p.Command.Group.Find("Nested Options") == nil {
|
||||
t.Errorf("Expected to find group `Nested Options'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupNestedInlineNamespace(t *testing.T) {
|
||||
var opts = struct {
|
||||
Opt string `long:"opt"`
|
||||
|
||||
Group struct {
|
||||
Opt string `long:"opt"`
|
||||
Group struct {
|
||||
Opt string `long:"opt"`
|
||||
} `group:"Subsubgroup" namespace:"sap"`
|
||||
} `group:"Subgroup" namespace:"sip"`
|
||||
}{}
|
||||
|
||||
p, ret := assertParserSuccess(t, &opts, "--opt", "a", "--sip.opt", "b", "--sip.sap.opt", "c", "rest")
|
||||
|
||||
assertStringArray(t, ret, []string{"rest"})
|
||||
|
||||
assertString(t, opts.Opt, "a")
|
||||
assertString(t, opts.Group.Opt, "b")
|
||||
assertString(t, opts.Group.Group.Opt, "c")
|
||||
|
||||
for _, name := range []string{"Subgroup", "Subsubgroup"} {
|
||||
if p.Command.Group.Find(name) == nil {
|
||||
t.Errorf("Expected to find group '%s'", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateShortFlags(t *testing.T) {
|
||||
var opts struct {
|
||||
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"`
|
||||
Variables []string `short:"v" long:"variable" description:"Set a variable value."`
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--verbose",
|
||||
"-v", "123",
|
||||
"-v", "456",
|
||||
}
|
||||
|
||||
_, err := ParseArgs(&opts, args)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error with type ErrDuplicatedFlag")
|
||||
} else {
|
||||
err2 := err.(*Error)
|
||||
if err2.Type != ErrDuplicatedFlag {
|
||||
t.Errorf("Expected an error with type ErrDuplicatedFlag")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateLongFlags(t *testing.T) {
|
||||
var opts struct {
|
||||
Test1 []bool `short:"a" long:"testing" description:"Test 1"`
|
||||
Test2 []string `short:"b" long:"testing" description:"Test 2."`
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--testing",
|
||||
}
|
||||
|
||||
_, err := ParseArgs(&opts, args)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error with type ErrDuplicatedFlag")
|
||||
} else {
|
||||
err2 := err.(*Error)
|
||||
if err2.Type != ErrDuplicatedFlag {
|
||||
t.Errorf("Expected an error with type ErrDuplicatedFlag")
|
||||
}
|
||||
}
|
||||
}
|
||||
-283
@@ -1,283 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type helpOptions struct {
|
||||
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information" ini-name:"verbose"`
|
||||
Call func(string) `short:"c" description:"Call phone number" ini-name:"call"`
|
||||
PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"`
|
||||
EmptyDescription bool `long:"empty-description"`
|
||||
|
||||
Default string `long:"default" default:"Some\nvalue" description:"Test default value"`
|
||||
DefaultArray []string `long:"default-array" default:"Some value" default:"Other\tvalue" description:"Test default array value"`
|
||||
DefaultMap map[string]string `long:"default-map" default:"some:value" default:"another:value" description:"Testdefault map value"`
|
||||
EnvDefault1 string `long:"env-default1" default:"Some value" env:"ENV_DEFAULT" description:"Test env-default1 value"`
|
||||
EnvDefault2 string `long:"env-default2" env:"ENV_DEFAULT" description:"Test env-default2 value"`
|
||||
|
||||
OnlyIni string `ini-name:"only-ini" description:"Option only available in ini"`
|
||||
|
||||
Other struct {
|
||||
StringSlice []string `short:"s" default:"some" default:"value" description:"A slice of strings"`
|
||||
IntMap map[string]int `long:"intmap" default:"a:1" description:"A map from string to int" ini-name:"int-map"`
|
||||
} `group:"Other Options"`
|
||||
|
||||
Group struct {
|
||||
Opt string `long:"opt" description:"This is a subgroup option"`
|
||||
|
||||
Group struct {
|
||||
Opt string `long:"opt" description:"This is a subsubgroup option"`
|
||||
} `group:"Subsubgroup" namespace:"sap"`
|
||||
} `group:"Subgroup" namespace:"sip"`
|
||||
|
||||
Command struct {
|
||||
ExtraVerbose []bool `long:"extra-verbose" description:"Use for extra verbosity"`
|
||||
} `command:"command" alias:"cm" alias:"cmd" description:"A command"`
|
||||
|
||||
Args struct {
|
||||
Filename string `name:"filename" description:"A filename"`
|
||||
Num int `name:"num" description:"A number"`
|
||||
} `positional-args:"yes"`
|
||||
}
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
oldEnv := EnvSnapshot()
|
||||
defer oldEnv.Restore()
|
||||
os.Setenv("ENV_DEFAULT", "env-def")
|
||||
|
||||
var opts helpOptions
|
||||
p := NewNamedParser("TestHelp", HelpFlag)
|
||||
p.AddGroup("Application Options", "The application options", &opts)
|
||||
|
||||
_, err := p.ParseArgs([]string{"--help"})
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Expected help error")
|
||||
}
|
||||
|
||||
if e, ok := err.(*Error); !ok {
|
||||
t.Fatalf("Expected flags.Error, but got %T", err)
|
||||
} else {
|
||||
if e.Type != ErrHelp {
|
||||
t.Errorf("Expected flags.ErrHelp type, but got %s", e.Type)
|
||||
}
|
||||
|
||||
var expected string
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = `Usage:
|
||||
TestHelp [OPTIONS] [filename] [num] <command>
|
||||
|
||||
Application Options:
|
||||
/v, /verbose Show verbose debug information
|
||||
/c: Call phone number
|
||||
/ptrslice: A slice of pointers to string
|
||||
/empty-description
|
||||
/default: Test default value ("Some\nvalue")
|
||||
/default-array: Test default array value (Some value, "Other\tvalue")
|
||||
/default-map: Testdefault map value (some:value, another:value)
|
||||
/env-default1: Test env-default1 value (Some value) [%ENV_DEFAULT%]
|
||||
/env-default2: Test env-default2 value [%ENV_DEFAULT%]
|
||||
|
||||
Other Options:
|
||||
/s: A slice of strings (some, value)
|
||||
/intmap: A map from string to int (a:1)
|
||||
|
||||
Subgroup:
|
||||
/sip.opt: This is a subgroup option
|
||||
|
||||
Subsubgroup:
|
||||
/sip.sap.opt: This is a subsubgroup option
|
||||
|
||||
Help Options:
|
||||
/? Show this help message
|
||||
/h, /help Show this help message
|
||||
|
||||
Arguments:
|
||||
filename: A filename
|
||||
num: A number
|
||||
|
||||
Available commands:
|
||||
command A command (aliases: cm, cmd)
|
||||
`
|
||||
} else {
|
||||
expected = `Usage:
|
||||
TestHelp [OPTIONS] [filename] [num] <command>
|
||||
|
||||
Application Options:
|
||||
-v, --verbose Show verbose debug information
|
||||
-c= Call phone number
|
||||
--ptrslice= A slice of pointers to string
|
||||
--empty-description
|
||||
--default= Test default value ("Some\nvalue")
|
||||
--default-array= Test default array value (Some value, "Other\tvalue")
|
||||
--default-map= Testdefault map value (some:value, another:value)
|
||||
--env-default1= Test env-default1 value (Some value) [$ENV_DEFAULT]
|
||||
--env-default2= Test env-default2 value [$ENV_DEFAULT]
|
||||
|
||||
Other Options:
|
||||
-s= A slice of strings (some, value)
|
||||
--intmap= A map from string to int (a:1)
|
||||
|
||||
Subgroup:
|
||||
--sip.opt= This is a subgroup option
|
||||
|
||||
Subsubgroup:
|
||||
--sip.sap.opt= This is a subsubgroup option
|
||||
|
||||
Help Options:
|
||||
-h, --help Show this help message
|
||||
|
||||
Arguments:
|
||||
filename: A filename
|
||||
num: A number
|
||||
|
||||
Available commands:
|
||||
command A command (aliases: cm, cmd)
|
||||
`
|
||||
}
|
||||
|
||||
assertDiff(t, e.Message, expected, "help message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMan(t *testing.T) {
|
||||
oldEnv := EnvSnapshot()
|
||||
defer oldEnv.Restore()
|
||||
os.Setenv("ENV_DEFAULT", "env-def")
|
||||
|
||||
var opts helpOptions
|
||||
p := NewNamedParser("TestMan", HelpFlag)
|
||||
p.ShortDescription = "Test manpage generation"
|
||||
p.LongDescription = "This is a somewhat `longer' description of what this does"
|
||||
p.AddGroup("Application Options", "The application options", &opts)
|
||||
|
||||
p.Commands()[0].LongDescription = "Longer `command' description"
|
||||
|
||||
var buf bytes.Buffer
|
||||
p.WriteManPage(&buf)
|
||||
|
||||
got := buf.String()
|
||||
|
||||
tt := time.Now()
|
||||
|
||||
expected := fmt.Sprintf(`.TH TestMan 1 "%s"
|
||||
.SH NAME
|
||||
TestMan \- Test manpage generation
|
||||
.SH SYNOPSIS
|
||||
\fBTestMan\fP [OPTIONS]
|
||||
.SH DESCRIPTION
|
||||
This is a somewhat \fBlonger\fP description of what this does
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\fB-v, --verbose\fP
|
||||
Show verbose debug information
|
||||
.TP
|
||||
\fB-c\fP
|
||||
Call phone number
|
||||
.TP
|
||||
\fB--ptrslice\fP
|
||||
A slice of pointers to string
|
||||
.TP
|
||||
\fB--empty-description\fP
|
||||
.TP
|
||||
\fB--default\fP
|
||||
Test default value
|
||||
.TP
|
||||
\fB--default-array\fP
|
||||
Test default array value
|
||||
.TP
|
||||
\fB--default-map\fP
|
||||
Testdefault map value
|
||||
.TP
|
||||
\fB--env-default1\fP
|
||||
Test env-default1 value
|
||||
.TP
|
||||
\fB--env-default2\fP
|
||||
Test env-default2 value
|
||||
.TP
|
||||
\fB-s\fP
|
||||
A slice of strings
|
||||
.TP
|
||||
\fB--intmap\fP
|
||||
A map from string to int
|
||||
.TP
|
||||
\fB--sip.opt\fP
|
||||
This is a subgroup option
|
||||
.TP
|
||||
\fB--sip.sap.opt\fP
|
||||
This is a subsubgroup option
|
||||
.SH COMMANDS
|
||||
.SS command
|
||||
A command
|
||||
|
||||
Longer \fBcommand\fP description
|
||||
|
||||
\fBUsage\fP: TestMan [OPTIONS] command [command-OPTIONS]
|
||||
|
||||
|
||||
\fBAliases\fP: cm, cmd
|
||||
|
||||
.TP
|
||||
\fB--extra-verbose\fP
|
||||
Use for extra verbosity
|
||||
`, tt.Format("2 January 2006"))
|
||||
|
||||
assertDiff(t, got, expected, "man page")
|
||||
}
|
||||
|
||||
type helpCommandNoOptions struct {
|
||||
Command struct {
|
||||
} `command:"command" description:"A command"`
|
||||
}
|
||||
|
||||
func TestHelpCommand(t *testing.T) {
|
||||
oldEnv := EnvSnapshot()
|
||||
defer oldEnv.Restore()
|
||||
os.Setenv("ENV_DEFAULT", "env-def")
|
||||
|
||||
var opts helpCommandNoOptions
|
||||
p := NewNamedParser("TestHelpCommand", HelpFlag)
|
||||
p.AddGroup("Application Options", "The application options", &opts)
|
||||
|
||||
_, err := p.ParseArgs([]string{"command", "--help"})
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Expected help error")
|
||||
}
|
||||
|
||||
if e, ok := err.(*Error); !ok {
|
||||
t.Fatalf("Expected flags.Error, but got %T", err)
|
||||
} else {
|
||||
if e.Type != ErrHelp {
|
||||
t.Errorf("Expected flags.ErrHelp type, but got %s", e.Type)
|
||||
}
|
||||
|
||||
var expected string
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = `Usage:
|
||||
TestHelpCommand [OPTIONS] command
|
||||
|
||||
Help Options:
|
||||
/? Show this help message
|
||||
/h, /help Show this help message
|
||||
`
|
||||
} else {
|
||||
expected = `Usage:
|
||||
TestHelpCommand [OPTIONS] command
|
||||
|
||||
Help Options:
|
||||
-h, --help Show this help message
|
||||
`
|
||||
}
|
||||
|
||||
assertDiff(t, e.Message, expected, "help message")
|
||||
}
|
||||
}
|
||||
-731
@@ -1,731 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteIni(t *testing.T) {
|
||||
oldEnv := EnvSnapshot()
|
||||
defer oldEnv.Restore()
|
||||
os.Setenv("ENV_DEFAULT", "env-def")
|
||||
|
||||
var tests = []struct {
|
||||
args []string
|
||||
options IniOptions
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
[]string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "command"},
|
||||
IniDefault,
|
||||
`[Application Options]
|
||||
; Show verbose debug information
|
||||
verbose = true
|
||||
verbose = true
|
||||
|
||||
; Test env-default1 value
|
||||
EnvDefault1 = env-def
|
||||
|
||||
; Test env-default2 value
|
||||
EnvDefault2 = env-def
|
||||
|
||||
[Other Options]
|
||||
; A map from string to int
|
||||
int-map = a:2
|
||||
int-map = b:3
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
[]string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "command"},
|
||||
IniDefault | IniIncludeDefaults,
|
||||
`[Application Options]
|
||||
; Show verbose debug information
|
||||
verbose = true
|
||||
verbose = true
|
||||
|
||||
; A slice of pointers to string
|
||||
; PtrSlice =
|
||||
|
||||
EmptyDescription = false
|
||||
|
||||
; Test default value
|
||||
Default = "Some\nvalue"
|
||||
|
||||
; Test default array value
|
||||
DefaultArray = Some value
|
||||
DefaultArray = "Other\tvalue"
|
||||
|
||||
; Testdefault map value
|
||||
DefaultMap = another:value
|
||||
DefaultMap = some:value
|
||||
|
||||
; Test env-default1 value
|
||||
EnvDefault1 = env-def
|
||||
|
||||
; Test env-default2 value
|
||||
EnvDefault2 = env-def
|
||||
|
||||
; Option only available in ini
|
||||
only-ini =
|
||||
|
||||
[Other Options]
|
||||
; A slice of strings
|
||||
StringSlice = some
|
||||
StringSlice = value
|
||||
|
||||
; A map from string to int
|
||||
int-map = a:2
|
||||
int-map = b:3
|
||||
|
||||
[Subgroup]
|
||||
; This is a subgroup option
|
||||
Opt =
|
||||
|
||||
[Subsubgroup]
|
||||
; This is a subsubgroup option
|
||||
Opt =
|
||||
|
||||
[command]
|
||||
; Use for extra verbosity
|
||||
; ExtraVerbose =
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
[]string{"filename", "0", "command"},
|
||||
IniDefault | IniIncludeDefaults | IniCommentDefaults,
|
||||
`[Application Options]
|
||||
; Show verbose debug information
|
||||
; verbose =
|
||||
|
||||
; A slice of pointers to string
|
||||
; PtrSlice =
|
||||
|
||||
; EmptyDescription = false
|
||||
|
||||
; Test default value
|
||||
; Default = "Some\nvalue"
|
||||
|
||||
; Test default array value
|
||||
; DefaultArray = Some value
|
||||
; DefaultArray = "Other\tvalue"
|
||||
|
||||
; Testdefault map value
|
||||
; DefaultMap = another:value
|
||||
; DefaultMap = some:value
|
||||
|
||||
; Test env-default1 value
|
||||
EnvDefault1 = env-def
|
||||
|
||||
; Test env-default2 value
|
||||
EnvDefault2 = env-def
|
||||
|
||||
; Option only available in ini
|
||||
; only-ini =
|
||||
|
||||
[Other Options]
|
||||
; A slice of strings
|
||||
; StringSlice = some
|
||||
; StringSlice = value
|
||||
|
||||
; A map from string to int
|
||||
; int-map = a:1
|
||||
|
||||
[Subgroup]
|
||||
; This is a subgroup option
|
||||
; Opt =
|
||||
|
||||
[Subsubgroup]
|
||||
; This is a subsubgroup option
|
||||
; Opt =
|
||||
|
||||
[command]
|
||||
; Use for extra verbosity
|
||||
; ExtraVerbose =
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
[]string{"--default=New value", "--default-array=New value", "--default-map=new:value", "filename", "0", "command"},
|
||||
IniDefault | IniIncludeDefaults | IniCommentDefaults,
|
||||
`[Application Options]
|
||||
; Show verbose debug information
|
||||
; verbose =
|
||||
|
||||
; A slice of pointers to string
|
||||
; PtrSlice =
|
||||
|
||||
; EmptyDescription = false
|
||||
|
||||
; Test default value
|
||||
Default = New value
|
||||
|
||||
; Test default array value
|
||||
DefaultArray = New value
|
||||
|
||||
; Testdefault map value
|
||||
DefaultMap = new:value
|
||||
|
||||
; Test env-default1 value
|
||||
EnvDefault1 = env-def
|
||||
|
||||
; Test env-default2 value
|
||||
EnvDefault2 = env-def
|
||||
|
||||
; Option only available in ini
|
||||
; only-ini =
|
||||
|
||||
[Other Options]
|
||||
; A slice of strings
|
||||
; StringSlice = some
|
||||
; StringSlice = value
|
||||
|
||||
; A map from string to int
|
||||
; int-map = a:1
|
||||
|
||||
[Subgroup]
|
||||
; This is a subgroup option
|
||||
; Opt =
|
||||
|
||||
[Subsubgroup]
|
||||
; This is a subsubgroup option
|
||||
; Opt =
|
||||
|
||||
[command]
|
||||
; Use for extra verbosity
|
||||
; ExtraVerbose =
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var opts helpOptions
|
||||
|
||||
p := NewNamedParser("TestIni", Default)
|
||||
p.AddGroup("Application Options", "The application options", &opts)
|
||||
|
||||
_, err := p.ParseArgs(test.args)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
inip := NewIniParser(p)
|
||||
|
||||
var b bytes.Buffer
|
||||
inip.Write(&b, test.options)
|
||||
|
||||
got := b.String()
|
||||
expected := test.expected
|
||||
|
||||
msg := fmt.Sprintf("with arguments %+v and ini options %b", test.args, test.options)
|
||||
assertDiff(t, got, expected, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadIni(t *testing.T) {
|
||||
var opts helpOptions
|
||||
|
||||
p := NewNamedParser("TestIni", Default)
|
||||
p.AddGroup("Application Options", "The application options", &opts)
|
||||
|
||||
inip := NewIniParser(p)
|
||||
|
||||
inic := `
|
||||
; Show verbose debug information
|
||||
verbose = true
|
||||
verbose = true
|
||||
|
||||
DefaultMap = another:"value\n1"
|
||||
DefaultMap = some:value 2
|
||||
|
||||
[Application Options]
|
||||
; A slice of pointers to string
|
||||
; PtrSlice =
|
||||
|
||||
; Test default value
|
||||
Default = "New\nvalue"
|
||||
|
||||
; Test env-default1 value
|
||||
EnvDefault1 = New value
|
||||
|
||||
[Other Options]
|
||||
# A slice of strings
|
||||
StringSlice = "some\nvalue"
|
||||
StringSlice = another value
|
||||
|
||||
; A map from string to int
|
||||
int-map = a:2
|
||||
int-map = b:3
|
||||
|
||||
`
|
||||
|
||||
b := strings.NewReader(inic)
|
||||
err := inip.Parse(b)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
assertBoolArray(t, opts.Verbose, []bool{true, true})
|
||||
|
||||
if v := map[string]string{"another": "value\n1", "some": "value 2"}; !reflect.DeepEqual(opts.DefaultMap, v) {
|
||||
t.Fatalf("Expected %#v for DefaultMap but got %#v", v, opts.DefaultMap)
|
||||
}
|
||||
|
||||
assertString(t, opts.Default, "New\nvalue")
|
||||
|
||||
assertString(t, opts.EnvDefault1, "New value")
|
||||
|
||||
assertStringArray(t, opts.Other.StringSlice, []string{"some\nvalue", "another value"})
|
||||
|
||||
if v, ok := opts.Other.IntMap["a"]; !ok {
|
||||
t.Errorf("Expected \"a\" in Other.IntMap")
|
||||
} else if v != 2 {
|
||||
t.Errorf("Expected Other.IntMap[\"a\"] = 2, but got %v", v)
|
||||
}
|
||||
|
||||
if v, ok := opts.Other.IntMap["b"]; !ok {
|
||||
t.Errorf("Expected \"b\" in Other.IntMap")
|
||||
} else if v != 3 {
|
||||
t.Errorf("Expected Other.IntMap[\"b\"] = 3, but got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAndWriteIni(t *testing.T) {
|
||||
var tests = []struct {
|
||||
options IniOptions
|
||||
read string
|
||||
write string
|
||||
}{
|
||||
{
|
||||
IniIncludeComments,
|
||||
`[Application Options]
|
||||
; Show verbose debug information
|
||||
verbose = true
|
||||
verbose = true
|
||||
|
||||
; Test default value
|
||||
Default = "quote me"
|
||||
|
||||
; Test default array value
|
||||
DefaultArray = 1
|
||||
DefaultArray = "2"
|
||||
DefaultArray = 3
|
||||
|
||||
; Testdefault map value
|
||||
; DefaultMap =
|
||||
|
||||
; Test env-default1 value
|
||||
EnvDefault1 = env-def
|
||||
|
||||
; Test env-default2 value
|
||||
EnvDefault2 = env-def
|
||||
|
||||
[Other Options]
|
||||
; A slice of strings
|
||||
; StringSlice =
|
||||
|
||||
; A map from string to int
|
||||
int-map = a:2
|
||||
int-map = b:"3"
|
||||
|
||||
`,
|
||||
`[Application Options]
|
||||
; Show verbose debug information
|
||||
verbose = true
|
||||
verbose = true
|
||||
|
||||
; Test default value
|
||||
Default = "quote me"
|
||||
|
||||
; Test default array value
|
||||
DefaultArray = 1
|
||||
DefaultArray = 2
|
||||
DefaultArray = 3
|
||||
|
||||
; Testdefault map value
|
||||
; DefaultMap =
|
||||
|
||||
; Test env-default1 value
|
||||
EnvDefault1 = env-def
|
||||
|
||||
; Test env-default2 value
|
||||
EnvDefault2 = env-def
|
||||
|
||||
[Other Options]
|
||||
; A slice of strings
|
||||
; StringSlice =
|
||||
|
||||
; A map from string to int
|
||||
int-map = a:2
|
||||
int-map = b:3
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
IniIncludeComments,
|
||||
`[Application Options]
|
||||
; Show verbose debug information
|
||||
verbose = true
|
||||
verbose = true
|
||||
|
||||
; Test default value
|
||||
Default = "quote me"
|
||||
|
||||
; Test default array value
|
||||
DefaultArray = "1"
|
||||
DefaultArray = "2"
|
||||
DefaultArray = "3"
|
||||
|
||||
; Testdefault map value
|
||||
; DefaultMap =
|
||||
|
||||
; Test env-default1 value
|
||||
EnvDefault1 = env-def
|
||||
|
||||
; Test env-default2 value
|
||||
EnvDefault2 = env-def
|
||||
|
||||
[Other Options]
|
||||
; A slice of strings
|
||||
; StringSlice =
|
||||
|
||||
; A map from string to int
|
||||
int-map = a:"2"
|
||||
int-map = b:"3"
|
||||
|
||||
`,
|
||||
`[Application Options]
|
||||
; Show verbose debug information
|
||||
verbose = true
|
||||
verbose = true
|
||||
|
||||
; Test default value
|
||||
Default = "quote me"
|
||||
|
||||
; Test default array value
|
||||
DefaultArray = "1"
|
||||
DefaultArray = "2"
|
||||
DefaultArray = "3"
|
||||
|
||||
; Testdefault map value
|
||||
; DefaultMap =
|
||||
|
||||
; Test env-default1 value
|
||||
EnvDefault1 = env-def
|
||||
|
||||
; Test env-default2 value
|
||||
EnvDefault2 = env-def
|
||||
|
||||
[Other Options]
|
||||
; A slice of strings
|
||||
; StringSlice =
|
||||
|
||||
; A map from string to int
|
||||
int-map = a:"2"
|
||||
int-map = b:"3"
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var opts helpOptions
|
||||
|
||||
p := NewNamedParser("TestIni", Default)
|
||||
p.AddGroup("Application Options", "The application options", &opts)
|
||||
|
||||
inip := NewIniParser(p)
|
||||
|
||||
read := strings.NewReader(test.read)
|
||||
err := inip.Parse(read)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
var write bytes.Buffer
|
||||
inip.Write(&write, test.options)
|
||||
|
||||
got := write.String()
|
||||
|
||||
msg := fmt.Sprintf("with ini options %b", test.options)
|
||||
assertDiff(t, got, test.write, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadIniWrongQuoting(t *testing.T) {
|
||||
var tests = []struct {
|
||||
iniFile string
|
||||
lineNumber uint
|
||||
}{
|
||||
{
|
||||
iniFile: `Default = "New\nvalue`,
|
||||
lineNumber: 1,
|
||||
},
|
||||
{
|
||||
iniFile: `StringSlice = "New\nvalue`,
|
||||
lineNumber: 1,
|
||||
},
|
||||
{
|
||||
iniFile: `StringSlice = "New\nvalue"
|
||||
StringSlice = "Second\nvalue`,
|
||||
lineNumber: 2,
|
||||
},
|
||||
{
|
||||
iniFile: `DefaultMap = some:"value`,
|
||||
lineNumber: 1,
|
||||
},
|
||||
{
|
||||
iniFile: `DefaultMap = some:value
|
||||
DefaultMap = another:"value`,
|
||||
lineNumber: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var opts helpOptions
|
||||
|
||||
p := NewNamedParser("TestIni", Default)
|
||||
p.AddGroup("Application Options", "The application options", &opts)
|
||||
|
||||
inip := NewIniParser(p)
|
||||
|
||||
inic := test.iniFile
|
||||
|
||||
b := strings.NewReader(inic)
|
||||
err := inip.Parse(b)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Expect error")
|
||||
}
|
||||
|
||||
iniError := err.(*IniError)
|
||||
|
||||
if iniError.LineNumber != test.lineNumber {
|
||||
t.Fatalf("Expect error on line %d", test.lineNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIniCommands(t *testing.T) {
|
||||
var opts struct {
|
||||
Value string `short:"v" long:"value"`
|
||||
|
||||
Add struct {
|
||||
Name int `short:"n" long:"name" ini-name:"AliasName"`
|
||||
|
||||
Other struct {
|
||||
O string `short:"o" long:"other"`
|
||||
} `group:"Other Options"`
|
||||
} `command:"add"`
|
||||
}
|
||||
|
||||
p := NewNamedParser("TestIni", Default)
|
||||
p.AddGroup("Application Options", "The application options", &opts)
|
||||
|
||||
inip := NewIniParser(p)
|
||||
|
||||
inic := `[Application Options]
|
||||
value = some value
|
||||
|
||||
[add]
|
||||
AliasName = 5
|
||||
|
||||
[add.Other Options]
|
||||
other = subgroup
|
||||
|
||||
`
|
||||
|
||||
b := strings.NewReader(inic)
|
||||
err := inip.Parse(b)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
assertString(t, opts.Value, "some value")
|
||||
|
||||
if opts.Add.Name != 5 {
|
||||
t.Errorf("Expected opts.Add.Name to be 5, but got %v", opts.Add.Name)
|
||||
}
|
||||
|
||||
assertString(t, opts.Add.Other.O, "subgroup")
|
||||
|
||||
// Test writing it back
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
inip.Write(buf, IniDefault)
|
||||
|
||||
assertDiff(t, buf.String(), inic, "ini contents")
|
||||
}
|
||||
|
||||
func TestIniNoIni(t *testing.T) {
|
||||
var opts struct {
|
||||
Value string `short:"v" long:"value" no-ini:"yes"`
|
||||
}
|
||||
|
||||
p := NewNamedParser("TestIni", Default)
|
||||
p.AddGroup("Application Options", "The application options", &opts)
|
||||
|
||||
inip := NewIniParser(p)
|
||||
|
||||
inic := `[Application Options]
|
||||
value = some value
|
||||
`
|
||||
|
||||
b := strings.NewReader(inic)
|
||||
err := inip.Parse(b)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error")
|
||||
}
|
||||
|
||||
iniError := err.(*IniError)
|
||||
|
||||
if v := uint(2); iniError.LineNumber != v {
|
||||
t.Errorf("Expected opts.Add.Name to be %d, but got %d", v, iniError.LineNumber)
|
||||
}
|
||||
|
||||
if v := "unknown option: value"; iniError.Message != v {
|
||||
t.Errorf("Expected opts.Add.Name to be %s, but got %s", v, iniError.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIniParse(t *testing.T) {
|
||||
file, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot create temporary file: %s", err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
_, err = file.WriteString("value = 123")
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot write to temporary file: %s", err)
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
var opts struct {
|
||||
Value int `long:"value"`
|
||||
}
|
||||
|
||||
err = IniParse(file.Name(), &opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse ini: %s", err)
|
||||
}
|
||||
|
||||
if opts.Value != 123 {
|
||||
t.Fatalf("Expected Value to be \"123\" but was \"%d\"", opts.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
file, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot create temporary file: %s", err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
var opts struct {
|
||||
Value int `long:"value"`
|
||||
}
|
||||
|
||||
opts.Value = 123
|
||||
|
||||
p := NewParser(&opts, Default)
|
||||
ini := NewIniParser(p)
|
||||
|
||||
err = ini.WriteFile(file.Name(), IniIncludeDefaults)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not write ini file: %s", err)
|
||||
}
|
||||
|
||||
found, err := ioutil.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read written ini file: %s", err)
|
||||
}
|
||||
|
||||
expected := "[Application Options]\nValue = 123\n\n"
|
||||
|
||||
assertDiff(t, string(found), expected, "ini content")
|
||||
}
|
||||
|
||||
func TestOverwriteRequiredOptions(t *testing.T) {
|
||||
var tests = []struct {
|
||||
args []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
args: []string{"--value", "from CLI"},
|
||||
expected: []string{
|
||||
"from CLI",
|
||||
"from default",
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--value", "from CLI", "--default", "from CLI"},
|
||||
expected: []string{
|
||||
"from CLI",
|
||||
"from CLI",
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--config", "no file name"},
|
||||
expected: []string{
|
||||
"from INI",
|
||||
"from INI",
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--value", "from CLI before", "--default", "from CLI before", "--config", "no file name"},
|
||||
expected: []string{
|
||||
"from INI",
|
||||
"from INI",
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--value", "from CLI before", "--default", "from CLI before", "--config", "no file name", "--value", "from CLI after", "--default", "from CLI after"},
|
||||
expected: []string{
|
||||
"from CLI after",
|
||||
"from CLI after",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var opts struct {
|
||||
Config func(s string) error `long:"config" no-ini:"true"`
|
||||
Value string `long:"value" required:"true"`
|
||||
Default string `long:"default" required:"true" default:"from default"`
|
||||
}
|
||||
|
||||
p := NewParser(&opts, Default)
|
||||
|
||||
opts.Config = func(s string) error {
|
||||
ini := NewIniParser(p)
|
||||
|
||||
return ini.Parse(bytes.NewBufferString("value = from INI\ndefault = from INI"))
|
||||
}
|
||||
|
||||
_, err := p.ParseArgs(test.args)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %s with args %+v", err, test.args)
|
||||
}
|
||||
|
||||
if opts.Value != test.expected[0] {
|
||||
t.Fatalf("Expected Value to be \"%s\" but was \"%s\" with args %+v", test.expected[0], opts.Value, test.args)
|
||||
}
|
||||
|
||||
if opts.Default != test.expected[1] {
|
||||
t.Fatalf("Expected Default to be \"%s\" but was \"%s\" with args %+v", test.expected[1], opts.Default, test.args)
|
||||
}
|
||||
}
|
||||
}
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLong(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `long:"value"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "--value")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongArg(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value string `long:"value"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "--value", "value")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
|
||||
func TestLongArgEqual(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value string `long:"value"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "--value=value")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
|
||||
func TestLongDefault(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value string `long:"value" default:"value"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts)
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
|
||||
func TestLongOptional(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value string `long:"value" optional:"yes" optional-value:"value"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "--value")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
|
||||
func TestLongOptionalArg(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value string `long:"value" optional:"yes" optional-value:"value"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "--value", "no")
|
||||
|
||||
assertStringArray(t, ret, []string{"no"})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
|
||||
func TestLongOptionalArgEqual(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value string `long:"value" optional:"yes" optional-value:"value"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "--value=value", "no")
|
||||
|
||||
assertStringArray(t, ret, []string{"no"})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type marshalled bool
|
||||
|
||||
func (m *marshalled) UnmarshalFlag(value string) error {
|
||||
if value == "yes" {
|
||||
*m = true
|
||||
} else if value == "no" {
|
||||
*m = false
|
||||
} else {
|
||||
return fmt.Errorf("`%s' is not a valid value, please specify `yes' or `no'", value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m marshalled) MarshalFlag() (string, error) {
|
||||
if m {
|
||||
return "yes", nil
|
||||
}
|
||||
|
||||
return "no", nil
|
||||
}
|
||||
|
||||
type marshalledError bool
|
||||
|
||||
func (m marshalledError) MarshalFlag() (string, error) {
|
||||
return "", newErrorf(ErrMarshal, "Failed to marshal")
|
||||
}
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value marshalled `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v=yes")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalDefault(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value marshalled `short:"v" default:"yes"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts)
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalOptional(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value marshalled `short:"v" optional:"yes" optional-value:"yes"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalError(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value marshalled `short:"v"`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrMarshal, fmt.Sprintf("invalid argument for flag `%cv' (expected flags.marshalled): `invalid' is not a valid value, please specify `yes' or `no'", defaultShortOptDelimiter), &opts, "-vinvalid")
|
||||
}
|
||||
|
||||
func TestMarshalError(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value marshalledError `short:"v"`
|
||||
}{}
|
||||
|
||||
p := NewParser(&opts, Default)
|
||||
o := p.Command.Groups()[0].Options()[0]
|
||||
|
||||
_, err := convertToString(o.value, o.tag)
|
||||
|
||||
assertError(t, err, ErrMarshal, "Failed to marshal")
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPassDoubleDash(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
}{}
|
||||
|
||||
p := NewParser(&opts, PassDoubleDash)
|
||||
ret, err := p.ParseArgs([]string{"-v", "--", "-v", "-g"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
|
||||
assertStringArray(t, ret, []string{"-v", "-g"})
|
||||
}
|
||||
|
||||
func TestPassAfterNonOption(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
}{}
|
||||
|
||||
p := NewParser(&opts, PassAfterNonOption)
|
||||
ret, err := p.ParseArgs([]string{"-v", "arg", "-v", "-g"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
|
||||
assertStringArray(t, ret, []string{"arg", "-v", "-g"})
|
||||
}
|
||||
-347
@@ -1,347 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type defaultOptions struct {
|
||||
Int int `long:"i"`
|
||||
IntDefault int `long:"id" default:"1"`
|
||||
|
||||
String string `long:"str"`
|
||||
StringDefault string `long:"strd" default:"abc"`
|
||||
StringNotUnquoted string `long:"strnot" unquote:"false"`
|
||||
|
||||
Time time.Duration `long:"t"`
|
||||
TimeDefault time.Duration `long:"td" default:"1m"`
|
||||
|
||||
Map map[string]int `long:"m"`
|
||||
MapDefault map[string]int `long:"md" default:"a:1"`
|
||||
|
||||
Slice []int `long:"s"`
|
||||
SliceDefault []int `long:"sd" default:"1" default:"2"`
|
||||
}
|
||||
|
||||
func TestDefaults(t *testing.T) {
|
||||
var tests = []struct {
|
||||
msg string
|
||||
args []string
|
||||
expected defaultOptions
|
||||
}{
|
||||
{
|
||||
msg: "no arguments, expecting default values",
|
||||
args: []string{},
|
||||
expected: defaultOptions{
|
||||
Int: 0,
|
||||
IntDefault: 1,
|
||||
|
||||
String: "",
|
||||
StringDefault: "abc",
|
||||
|
||||
Time: 0,
|
||||
TimeDefault: time.Minute,
|
||||
|
||||
Map: map[string]int{},
|
||||
MapDefault: map[string]int{"a": 1},
|
||||
|
||||
Slice: []int{},
|
||||
SliceDefault: []int{1, 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
msg: "non-zero value arguments, expecting overwritten arguments",
|
||||
args: []string{"--i=3", "--id=3", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"},
|
||||
expected: defaultOptions{
|
||||
Int: 3,
|
||||
IntDefault: 3,
|
||||
|
||||
String: "def",
|
||||
StringDefault: "def",
|
||||
|
||||
Time: 3 * time.Millisecond,
|
||||
TimeDefault: 3 * time.Millisecond,
|
||||
|
||||
Map: map[string]int{"c": 3},
|
||||
MapDefault: map[string]int{"c": 3},
|
||||
|
||||
Slice: []int{3},
|
||||
SliceDefault: []int{3},
|
||||
},
|
||||
},
|
||||
{
|
||||
msg: "zero value arguments, expecting overwritten arguments",
|
||||
args: []string{"--i=0", "--id=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"},
|
||||
expected: defaultOptions{
|
||||
Int: 0,
|
||||
IntDefault: 0,
|
||||
|
||||
String: "",
|
||||
StringDefault: "",
|
||||
|
||||
Time: 0,
|
||||
TimeDefault: 0,
|
||||
|
||||
Map: map[string]int{"": 0},
|
||||
MapDefault: map[string]int{"": 0},
|
||||
|
||||
Slice: []int{0},
|
||||
SliceDefault: []int{0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var opts defaultOptions
|
||||
|
||||
_, err := ParseArgs(&opts, test.args)
|
||||
if err != nil {
|
||||
t.Fatalf("%s:\nUnexpected error: %v", test.msg, err)
|
||||
}
|
||||
|
||||
if opts.Slice == nil {
|
||||
opts.Slice = []int{}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(opts, test.expected) {
|
||||
t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnquoting(t *testing.T) {
|
||||
var tests = []struct {
|
||||
arg string
|
||||
err error
|
||||
value string
|
||||
}{
|
||||
{
|
||||
arg: "\"abc",
|
||||
err: strconv.ErrSyntax,
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
arg: "\"\"abc\"",
|
||||
err: strconv.ErrSyntax,
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
arg: "\"abc\"",
|
||||
err: nil,
|
||||
value: "abc",
|
||||
},
|
||||
{
|
||||
arg: "\"\\\"abc\\\"\"",
|
||||
err: nil,
|
||||
value: "\"abc\"",
|
||||
},
|
||||
{
|
||||
arg: "\"\\\"abc\"",
|
||||
err: nil,
|
||||
value: "\"abc",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var opts defaultOptions
|
||||
|
||||
for _, delimiter := range []bool{false, true} {
|
||||
p := NewParser(&opts, None)
|
||||
|
||||
var err error
|
||||
if delimiter {
|
||||
_, err = p.ParseArgs([]string{"--str=" + test.arg, "--strnot=" + test.arg})
|
||||
} else {
|
||||
_, err = p.ParseArgs([]string{"--str", test.arg, "--strnot", test.arg})
|
||||
}
|
||||
|
||||
if test.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if test.value != opts.String {
|
||||
t.Fatalf("Expected String to be %q but got %q", test.value, opts.String)
|
||||
}
|
||||
if q := strconv.Quote(test.value); q != opts.StringNotUnquoted {
|
||||
t.Fatalf("Expected StringDefault to be %q but got %q", q, opts.StringNotUnquoted)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error")
|
||||
} else if e, ok := err.(*Error); ok {
|
||||
if strings.HasPrefix(e.Message, test.err.Error()) {
|
||||
t.Fatalf("Expected error message to end with %q but got %v", test.err.Error(), e.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// envRestorer keeps a copy of a set of env variables and can restore the env from them
|
||||
type envRestorer struct {
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
func (r *envRestorer) Restore() {
|
||||
os.Clearenv()
|
||||
for k, v := range r.env {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// EnvSnapshot returns a snapshot of the currently set env variables
|
||||
func EnvSnapshot() *envRestorer {
|
||||
r := envRestorer{make(map[string]string)}
|
||||
for _, kv := range os.Environ() {
|
||||
parts := strings.SplitN(kv, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
panic("got a weird env variable: " + kv)
|
||||
}
|
||||
r.env[parts[0]] = parts[1]
|
||||
}
|
||||
return &r
|
||||
}
|
||||
|
||||
type envDefaultOptions struct {
|
||||
Int int `long:"i" default:"1" env:"TEST_I"`
|
||||
Time time.Duration `long:"t" default:"1m" env:"TEST_T"`
|
||||
Map map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"`
|
||||
Slice []int `long:"s" default:"1" default:"2" env:"TEST_S" env-delim:","`
|
||||
}
|
||||
|
||||
func TestEnvDefaults(t *testing.T) {
|
||||
var tests = []struct {
|
||||
msg string
|
||||
args []string
|
||||
expected envDefaultOptions
|
||||
env map[string]string
|
||||
}{
|
||||
{
|
||||
msg: "no arguments, no env, expecting default values",
|
||||
args: []string{},
|
||||
expected: envDefaultOptions{
|
||||
Int: 1,
|
||||
Time: time.Minute,
|
||||
Map: map[string]int{"a": 1},
|
||||
Slice: []int{1, 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
msg: "no arguments, env defaults, expecting env default values",
|
||||
args: []string{},
|
||||
expected: envDefaultOptions{
|
||||
Int: 2,
|
||||
Time: 2 * time.Minute,
|
||||
Map: map[string]int{"a": 2, "b": 3},
|
||||
Slice: []int{4, 5, 6},
|
||||
},
|
||||
env: map[string]string{
|
||||
"TEST_I": "2",
|
||||
"TEST_T": "2m",
|
||||
"TEST_M": "a:2;b:3",
|
||||
"TEST_S": "4,5,6",
|
||||
},
|
||||
},
|
||||
{
|
||||
msg: "non-zero value arguments, expecting overwritten arguments",
|
||||
args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3"},
|
||||
expected: envDefaultOptions{
|
||||
Int: 3,
|
||||
Time: 3 * time.Millisecond,
|
||||
Map: map[string]int{"c": 3},
|
||||
Slice: []int{3},
|
||||
},
|
||||
env: map[string]string{
|
||||
"TEST_I": "2",
|
||||
"TEST_T": "2m",
|
||||
"TEST_M": "a:2;b:3",
|
||||
"TEST_S": "4,5,6",
|
||||
},
|
||||
},
|
||||
{
|
||||
msg: "zero value arguments, expecting overwritten arguments",
|
||||
args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0"},
|
||||
expected: envDefaultOptions{
|
||||
Int: 0,
|
||||
Time: 0,
|
||||
Map: map[string]int{"": 0},
|
||||
Slice: []int{0},
|
||||
},
|
||||
env: map[string]string{
|
||||
"TEST_I": "2",
|
||||
"TEST_T": "2m",
|
||||
"TEST_M": "a:2;b:3",
|
||||
"TEST_S": "4,5,6",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
oldEnv := EnvSnapshot()
|
||||
defer oldEnv.Restore()
|
||||
|
||||
for _, test := range tests {
|
||||
var opts envDefaultOptions
|
||||
oldEnv.Restore()
|
||||
for envKey, envValue := range test.env {
|
||||
os.Setenv(envKey, envValue)
|
||||
}
|
||||
_, err := ParseArgs(&opts, test.args)
|
||||
if err != nil {
|
||||
t.Fatalf("%s:\nUnexpected error: %v", test.msg, err)
|
||||
}
|
||||
|
||||
if opts.Slice == nil {
|
||||
opts.Slice = []int{}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(opts, test.expected) {
|
||||
t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionAsArgument(t *testing.T) {
|
||||
var tests = []struct {
|
||||
args []string
|
||||
expectError bool
|
||||
errType ErrorType
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
// short option must not be accepted as argument
|
||||
args: []string{"--string-slice", "foobar", "--string-slice", "-o"},
|
||||
expectError: true,
|
||||
errType: ErrExpectedArgument,
|
||||
errMsg: "expected argument for flag `--string-slice', but got option `-o'",
|
||||
},
|
||||
{
|
||||
// long option must not be accepted as argument
|
||||
args: []string{"--string-slice", "foobar", "--string-slice", "--other-option"},
|
||||
expectError: true,
|
||||
errType: ErrExpectedArgument,
|
||||
errMsg: "expected argument for flag `--string-slice', but got option `--other-option'",
|
||||
},
|
||||
{
|
||||
// quoted and appended option should be accepted as argument (even if it looks like an option)
|
||||
args: []string{"--string-slice", "foobar", "--string-slice=\"--other-option\""},
|
||||
},
|
||||
}
|
||||
var opts struct {
|
||||
StringSlice []string `long:"string-slice"`
|
||||
OtherOption bool `long:"other-option" short:"o"`
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if test.expectError {
|
||||
assertParseFail(t, test.errType, test.errMsg, &opts, test.args...)
|
||||
} else {
|
||||
assertParseSuccess(t, &opts, test.args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPointerBool(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value *bool `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if !*opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointerString(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value *string `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v", "value")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertString(t, *opts.Value, "value")
|
||||
}
|
||||
|
||||
func TestPointerSlice(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value *[]string `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v", "value1", "-v", "value2")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertStringArray(t, *opts.Value, []string{"value1", "value2"})
|
||||
}
|
||||
|
||||
func TestPointerMap(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value *map[string]int `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v", "k1:2", "-v", "k2:-5")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if v, ok := (*opts.Value)["k1"]; !ok {
|
||||
t.Errorf("Expected key \"k1\" to exist")
|
||||
} else if v != 2 {
|
||||
t.Errorf("Expected \"k1\" to be 2, but got %#v", v)
|
||||
}
|
||||
|
||||
if v, ok := (*opts.Value)["k2"]; !ok {
|
||||
t.Errorf("Expected key \"k2\" to exist")
|
||||
} else if v != -5 {
|
||||
t.Errorf("Expected \"k2\" to be -5, but got %#v", v)
|
||||
}
|
||||
}
|
||||
|
||||
type PointerGroup struct {
|
||||
Value bool `short:"v"`
|
||||
}
|
||||
|
||||
func TestPointerGroup(t *testing.T) {
|
||||
var opts = struct {
|
||||
Group *PointerGroup `group:"Group Options"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if !opts.Group.Value {
|
||||
t.Errorf("Expected Group.Value to be true")
|
||||
}
|
||||
}
|
||||
-194
@@ -1,194 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestShort(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if !opts.Value {
|
||||
t.Errorf("Expected Value to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortTooLong(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"vv"`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrShortNameTooLong, "short names can only be 1 character long, not `vv'", &opts)
|
||||
}
|
||||
|
||||
func TestShortRequired(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v" required:"true"`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrRequired, fmt.Sprintf("the required flag `%cv' was not specified", defaultShortOptDelimiter), &opts)
|
||||
}
|
||||
|
||||
func TestShortMultiConcat(t *testing.T) {
|
||||
var opts = struct {
|
||||
V bool `short:"v"`
|
||||
O bool `short:"o"`
|
||||
F bool `short:"f"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-vo", "-f")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if !opts.V {
|
||||
t.Errorf("Expected V to be true")
|
||||
}
|
||||
|
||||
if !opts.O {
|
||||
t.Errorf("Expected O to be true")
|
||||
}
|
||||
|
||||
if !opts.F {
|
||||
t.Errorf("Expected F to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortMultiRequiredConcat(t *testing.T) {
|
||||
var opts = struct {
|
||||
V bool `short:"v" required:"true"`
|
||||
O bool `short:"o" required:"true"`
|
||||
F bool `short:"f" required:"true"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-vo", "-f")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
|
||||
if !opts.V {
|
||||
t.Errorf("Expected V to be true")
|
||||
}
|
||||
|
||||
if !opts.O {
|
||||
t.Errorf("Expected O to be true")
|
||||
}
|
||||
|
||||
if !opts.F {
|
||||
t.Errorf("Expected F to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortMultiSlice(t *testing.T) {
|
||||
var opts = struct {
|
||||
Values []bool `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v", "-v")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertBoolArray(t, opts.Values, []bool{true, true})
|
||||
}
|
||||
|
||||
func TestShortMultiSliceConcat(t *testing.T) {
|
||||
var opts = struct {
|
||||
Values []bool `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-vvv")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertBoolArray(t, opts.Values, []bool{true, true, true})
|
||||
}
|
||||
|
||||
func TestShortWithEqualArg(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value string `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v=value")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
|
||||
func TestShortWithArg(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value string `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-vvalue")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
|
||||
func TestShortArg(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value string `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-v", "value")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
|
||||
func TestShortMultiWithEqualArg(t *testing.T) {
|
||||
var opts = struct {
|
||||
F []bool `short:"f"`
|
||||
Value string `short:"v"`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrExpectedArgument, fmt.Sprintf("expected argument for flag `%cv'", defaultShortOptDelimiter), &opts, "-ffv=value")
|
||||
}
|
||||
|
||||
func TestShortMultiArg(t *testing.T) {
|
||||
var opts = struct {
|
||||
F []bool `short:"f"`
|
||||
Value string `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-ffv", "value")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertBoolArray(t, opts.F, []bool{true, true})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
|
||||
func TestShortMultiArgConcatFail(t *testing.T) {
|
||||
var opts = struct {
|
||||
F []bool `short:"f"`
|
||||
Value string `short:"v"`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrExpectedArgument, fmt.Sprintf("expected argument for flag `%cv'", defaultShortOptDelimiter), &opts, "-ffvvalue")
|
||||
}
|
||||
|
||||
func TestShortMultiArgConcat(t *testing.T) {
|
||||
var opts = struct {
|
||||
F []bool `short:"f"`
|
||||
Value string `short:"v"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-vff")
|
||||
|
||||
assertStringArray(t, ret, []string{})
|
||||
assertString(t, opts.Value, "ff")
|
||||
}
|
||||
|
||||
func TestShortOptional(t *testing.T) {
|
||||
var opts = struct {
|
||||
F []bool `short:"f"`
|
||||
Value string `short:"v" optional:"yes" optional-value:"value"`
|
||||
}{}
|
||||
|
||||
ret := assertParseSuccess(t, &opts, "-fv", "f")
|
||||
|
||||
assertStringArray(t, ret, []string{"f"})
|
||||
assertString(t, opts.Value, "value")
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTagMissingColon(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrTag, "expected `:' after key name, but got end of tag (in `short`)", &opts, "")
|
||||
}
|
||||
|
||||
func TestTagMissingValue(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrTag, "expected `\"' to start tag value at end of tag (in `short:`)", &opts, "")
|
||||
}
|
||||
|
||||
func TestTagMissingQuote(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `short:"v`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrTag, "expected end of tag value `\"' at end of tag (in `short:\"v`)", &opts, "")
|
||||
}
|
||||
|
||||
func TestTagNewline(t *testing.T) {
|
||||
var opts = struct {
|
||||
Value bool `long:"verbose" description:"verbose
|
||||
something"`
|
||||
}{}
|
||||
|
||||
assertParseFail(t, ErrTag, "unexpected newline in tag value `description' (in `long:\"verbose\" description:\"verbose\nsomething\"`)", &opts, "")
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnknownFlags(t *testing.T) {
|
||||
var opts = struct {
|
||||
Verbose []bool `short:"v" long:"verbose" description:"Verbose output"`
|
||||
}{}
|
||||
|
||||
args := []string{
|
||||
"-f",
|
||||
}
|
||||
|
||||
p := NewParser(&opts, 0)
|
||||
args, err := p.ParseArgs(args)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for unknown argument")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoreUnknownFlags(t *testing.T) {
|
||||
var opts = struct {
|
||||
Verbose []bool `short:"v" long:"verbose" description:"Verbose output"`
|
||||
}{}
|
||||
|
||||
args := []string{
|
||||
"hello",
|
||||
"world",
|
||||
"-v",
|
||||
"--foo=bar",
|
||||
"--verbose",
|
||||
"-f",
|
||||
}
|
||||
|
||||
p := NewParser(&opts, IgnoreUnknown)
|
||||
args, err := p.ParseArgs(args)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
exargs := []string{
|
||||
"hello",
|
||||
"world",
|
||||
"--foo=bar",
|
||||
"-f",
|
||||
}
|
||||
|
||||
issame := (len(args) == len(exargs))
|
||||
|
||||
if issame {
|
||||
for i := 0; i < len(args); i++ {
|
||||
if args[i] != exargs[i] {
|
||||
issame = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !issame {
|
||||
t.Fatalf("Expected %v but got %v", exargs, args)
|
||||
}
|
||||
}
|
||||
-227
@@ -1,227 +0,0 @@
|
||||
package sqlx
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompileQuery(t *testing.T) {
|
||||
table := []struct {
|
||||
Q, R, D, N string
|
||||
V []string
|
||||
}{
|
||||
// basic test for named parameters, invalid char ',' terminating
|
||||
{
|
||||
Q: `INSERT INTO foo (a,b,c,d) VALUES (:name, :age, :first, :last)`,
|
||||
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
|
||||
D: `INSERT INTO foo (a,b,c,d) VALUES ($1, $2, $3, $4)`,
|
||||
N: `INSERT INTO foo (a,b,c,d) VALUES (:name, :age, :first, :last)`,
|
||||
V: []string{"name", "age", "first", "last"},
|
||||
},
|
||||
// This query tests a named parameter ending the string as well as numbers
|
||||
{
|
||||
Q: `SELECT * FROM a WHERE first_name=:name1 AND last_name=:name2`,
|
||||
R: `SELECT * FROM a WHERE first_name=? AND last_name=?`,
|
||||
D: `SELECT * FROM a WHERE first_name=$1 AND last_name=$2`,
|
||||
N: `SELECT * FROM a WHERE first_name=:name1 AND last_name=:name2`,
|
||||
V: []string{"name1", "name2"},
|
||||
},
|
||||
{
|
||||
Q: `SELECT "::foo" FROM a WHERE first_name=:name1 AND last_name=:name2`,
|
||||
R: `SELECT ":foo" FROM a WHERE first_name=? AND last_name=?`,
|
||||
D: `SELECT ":foo" FROM a WHERE first_name=$1 AND last_name=$2`,
|
||||
N: `SELECT ":foo" FROM a WHERE first_name=:name1 AND last_name=:name2`,
|
||||
V: []string{"name1", "name2"},
|
||||
},
|
||||
{
|
||||
Q: `SELECT 'a::b::c' || first_name, '::::ABC::_::' FROM person WHERE first_name=:first_name AND last_name=:last_name`,
|
||||
R: `SELECT 'a:b:c' || first_name, '::ABC:_:' FROM person WHERE first_name=? AND last_name=?`,
|
||||
D: `SELECT 'a:b:c' || first_name, '::ABC:_:' FROM person WHERE first_name=$1 AND last_name=$2`,
|
||||
N: `SELECT 'a:b:c' || first_name, '::ABC:_:' FROM person WHERE first_name=:first_name AND last_name=:last_name`,
|
||||
V: []string{"first_name", "last_name"},
|
||||
},
|
||||
/* This unicode awareness test sadly fails, because of our byte-wise worldview.
|
||||
* We could certainly iterate by Rune instead, though it's a great deal slower,
|
||||
* it's probably the RightWay(tm)
|
||||
{
|
||||
Q: `INSERT INTO foo (a,b,c,d) VALUES (:あ, :b, :キコ, :名前)`,
|
||||
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
|
||||
D: `INSERT INTO foo (a,b,c,d) VALUES ($1, $2, $3, $4)`,
|
||||
N: []string{"name", "age", "first", "last"},
|
||||
},
|
||||
*/
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
qr, names, err := compileNamedQuery([]byte(test.Q), QUESTION)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if qr != test.R {
|
||||
t.Errorf("expected %s, got %s", test.R, qr)
|
||||
}
|
||||
if len(names) != len(test.V) {
|
||||
t.Errorf("expected %#v, got %#v", test.V, names)
|
||||
} else {
|
||||
for i, name := range names {
|
||||
if name != test.V[i] {
|
||||
t.Errorf("expected %dth name to be %s, got %s", i+1, test.V[i], name)
|
||||
}
|
||||
}
|
||||
}
|
||||
qd, _, _ := compileNamedQuery([]byte(test.Q), DOLLAR)
|
||||
if qd != test.D {
|
||||
t.Errorf("\nexpected: `%s`\ngot: `%s`", test.D, qd)
|
||||
}
|
||||
|
||||
qq, _, _ := compileNamedQuery([]byte(test.Q), NAMED)
|
||||
if qq != test.N {
|
||||
t.Errorf("\nexpected: `%s`\ngot: `%s`\n(len: %d vs %d)", test.N, qq, len(test.N), len(qq))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Test struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (t Test) Error(err error, msg ...interface{}) {
|
||||
if err != nil {
|
||||
if len(msg) == 0 {
|
||||
t.t.Error(err)
|
||||
} else {
|
||||
t.t.Error(msg...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t Test) Errorf(err error, format string, args ...interface{}) {
|
||||
if err != nil {
|
||||
t.t.Errorf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedQueries(t *testing.T) {
|
||||
RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T) {
|
||||
loadDefaultFixture(db, t)
|
||||
test := Test{t}
|
||||
var ns *NamedStmt
|
||||
var err error
|
||||
|
||||
// Check that invalid preparations fail
|
||||
ns, err = db.PrepareNamed("SELECT * FROM person WHERE first_name=:first:name")
|
||||
if err == nil {
|
||||
t.Error("Expected an error with invalid prepared statement.")
|
||||
}
|
||||
|
||||
ns, err = db.PrepareNamed("invalid sql")
|
||||
if err == nil {
|
||||
t.Error("Expected an error with invalid prepared statement.")
|
||||
}
|
||||
|
||||
// Check closing works as anticipated
|
||||
ns, err = db.PrepareNamed("SELECT * FROM person WHERE first_name=:first_name")
|
||||
test.Error(err)
|
||||
err = ns.Close()
|
||||
test.Error(err)
|
||||
|
||||
ns, err = db.PrepareNamed(`
|
||||
SELECT first_name, last_name, email
|
||||
FROM person WHERE first_name=:first_name AND email=:email`)
|
||||
test.Error(err)
|
||||
|
||||
// test Queryx w/ uses Query
|
||||
p := Person{FirstName: "Jason", LastName: "Moiron", Email: "jmoiron@jmoiron.net"}
|
||||
|
||||
rows, err := ns.Queryx(p)
|
||||
test.Error(err)
|
||||
for rows.Next() {
|
||||
var p2 Person
|
||||
rows.StructScan(&p2)
|
||||
if p.FirstName != p2.FirstName {
|
||||
t.Errorf("got %s, expected %s", p.FirstName, p2.FirstName)
|
||||
}
|
||||
if p.LastName != p2.LastName {
|
||||
t.Errorf("got %s, expected %s", p.LastName, p2.LastName)
|
||||
}
|
||||
if p.Email != p2.Email {
|
||||
t.Errorf("got %s, expected %s", p.Email, p2.Email)
|
||||
}
|
||||
}
|
||||
|
||||
// test Select
|
||||
people := make([]Person, 0, 5)
|
||||
err = ns.Select(&people, p)
|
||||
test.Error(err)
|
||||
|
||||
if len(people) != 1 {
|
||||
t.Errorf("got %d results, expected %d", len(people), 1)
|
||||
}
|
||||
if p.FirstName != people[0].FirstName {
|
||||
t.Errorf("got %s, expected %s", p.FirstName, people[0].FirstName)
|
||||
}
|
||||
if p.LastName != people[0].LastName {
|
||||
t.Errorf("got %s, expected %s", p.LastName, people[0].LastName)
|
||||
}
|
||||
if p.Email != people[0].Email {
|
||||
t.Errorf("got %s, expected %s", p.Email, people[0].Email)
|
||||
}
|
||||
|
||||
// test Exec
|
||||
ns, err = db.PrepareNamed(`
|
||||
INSERT INTO person (first_name, last_name, email)
|
||||
VALUES (:first_name, :last_name, :email)`)
|
||||
test.Error(err)
|
||||
|
||||
js := Person{
|
||||
FirstName: "Julien",
|
||||
LastName: "Savea",
|
||||
Email: "jsavea@ab.co.nz",
|
||||
}
|
||||
_, err = ns.Exec(js)
|
||||
test.Error(err)
|
||||
|
||||
// Make sure we can pull him out again
|
||||
p2 := Person{}
|
||||
db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), js.Email)
|
||||
if p2.Email != js.Email {
|
||||
t.Errorf("expected %s, got %s", js.Email, p2.Email)
|
||||
}
|
||||
|
||||
// test Txn NamedStmts
|
||||
tx := db.MustBegin()
|
||||
txns := tx.NamedStmt(ns)
|
||||
|
||||
// We're going to add Steven in this txn
|
||||
sl := Person{
|
||||
FirstName: "Steven",
|
||||
LastName: "Luatua",
|
||||
Email: "sluatua@ab.co.nz",
|
||||
}
|
||||
|
||||
_, err = txns.Exec(sl)
|
||||
test.Error(err)
|
||||
// then rollback...
|
||||
tx.Rollback()
|
||||
// looking for Steven after a rollback should fail
|
||||
err = db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), sl.Email)
|
||||
if err != sql.ErrNoRows {
|
||||
t.Errorf("expected no rows error, got %v", err)
|
||||
}
|
||||
|
||||
// now do the same, but commit
|
||||
tx = db.MustBegin()
|
||||
txns = tx.NamedStmt(ns)
|
||||
_, err = txns.Exec(sl)
|
||||
test.Error(err)
|
||||
tx.Commit()
|
||||
|
||||
// looking for Steven after a Commit should succeed
|
||||
err = db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), sl.Email)
|
||||
test.Error(err)
|
||||
if p2.Email != sl.Email {
|
||||
t.Errorf("expected %s, got %s", sl.Email, p2.Email)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
-216
@@ -1,216 +0,0 @@
|
||||
package reflectx
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ival(v reflect.Value) int {
|
||||
return v.Interface().(int)
|
||||
}
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
type Foo struct {
|
||||
A int
|
||||
B int
|
||||
C int
|
||||
}
|
||||
|
||||
f := Foo{1, 2, 3}
|
||||
fv := reflect.ValueOf(f)
|
||||
m := NewMapper("")
|
||||
|
||||
v := m.FieldByName(fv, "A")
|
||||
if ival(v) != f.A {
|
||||
t.Errorf("Expecting %d, got %d", ival(v), f.A)
|
||||
}
|
||||
v = m.FieldByName(fv, "B")
|
||||
if ival(v) != f.B {
|
||||
t.Errorf("Expecting %d, got %d", f.B, ival(v))
|
||||
}
|
||||
v = m.FieldByName(fv, "C")
|
||||
if ival(v) != f.C {
|
||||
t.Errorf("Expecting %d, got %d", f.C, ival(v))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbedded(t *testing.T) {
|
||||
type Foo struct {
|
||||
A int
|
||||
}
|
||||
|
||||
type Bar struct {
|
||||
Foo
|
||||
B int
|
||||
}
|
||||
|
||||
type Baz struct {
|
||||
A int
|
||||
Bar
|
||||
}
|
||||
|
||||
m := NewMapper("")
|
||||
|
||||
z := Baz{}
|
||||
z.A = 1
|
||||
z.B = 2
|
||||
z.Bar.Foo.A = 3
|
||||
zv := reflect.ValueOf(z)
|
||||
|
||||
v := m.FieldByName(zv, "A")
|
||||
if ival(v) != z.A {
|
||||
t.Errorf("Expecting %d, got %d", ival(v), z.A)
|
||||
}
|
||||
v = m.FieldByName(zv, "B")
|
||||
if ival(v) != z.B {
|
||||
t.Errorf("Expecting %d, got %d", ival(v), z.B)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapping(t *testing.T) {
|
||||
type Person struct {
|
||||
ID int
|
||||
Name string
|
||||
WearsGlasses bool `db:"wears_glasses"`
|
||||
}
|
||||
|
||||
m := NewMapperFunc("db", strings.ToLower)
|
||||
p := Person{1, "Jason", true}
|
||||
mapping := m.TypeMap(reflect.TypeOf(p))
|
||||
|
||||
for _, key := range []string{"id", "name", "wears_glasses"} {
|
||||
if _, ok := mapping[key]; !ok {
|
||||
t.Errorf("Expecting to find key %s in mapping but did not.", key)
|
||||
}
|
||||
}
|
||||
|
||||
type SportsPerson struct {
|
||||
Weight int
|
||||
Age int
|
||||
Person
|
||||
}
|
||||
s := SportsPerson{Weight: 100, Age: 30, Person: p}
|
||||
mapping = m.TypeMap(reflect.TypeOf(s))
|
||||
for _, key := range []string{"id", "name", "wears_glasses", "weight", "age"} {
|
||||
if _, ok := mapping[key]; !ok {
|
||||
t.Errorf("Expecting to find key %s in mapping but did not.", key)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type RugbyPlayer struct {
|
||||
Position int
|
||||
IsIntense bool `db:"is_intense"`
|
||||
IsAllBlack bool `db:"-"`
|
||||
SportsPerson
|
||||
}
|
||||
r := RugbyPlayer{12, true, false, s}
|
||||
mapping = m.TypeMap(reflect.TypeOf(r))
|
||||
for _, key := range []string{"id", "name", "wears_glasses", "weight", "age", "position", "is_intense"} {
|
||||
if _, ok := mapping[key]; !ok {
|
||||
t.Errorf("Expecting to find key %s in mapping but did not.", key)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := mapping["isallblack"]; ok {
|
||||
t.Errorf("Expecting to ignore `IsAllBlack` field")
|
||||
}
|
||||
|
||||
type EmbeddedLiteral struct {
|
||||
Embedded struct {
|
||||
Person string
|
||||
Position int
|
||||
}
|
||||
IsIntense bool
|
||||
}
|
||||
|
||||
e := EmbeddedLiteral{}
|
||||
mapping = m.TypeMap(reflect.TypeOf(e))
|
||||
//fmt.Printf("Mapping: %#v\n", mapping)
|
||||
|
||||
//f := FieldByIndexes(reflect.ValueOf(e), mapping["isintense"])
|
||||
//fmt.Println(f, f.Interface())
|
||||
|
||||
//tbn := m.TraversalsByName(reflect.TypeOf(e), []string{"isintense"})
|
||||
//fmt.Printf("%#v\n", tbn)
|
||||
|
||||
}
|
||||
|
||||
type E1 struct {
|
||||
A int
|
||||
}
|
||||
type E2 struct {
|
||||
E1
|
||||
B int
|
||||
}
|
||||
type E3 struct {
|
||||
E2
|
||||
C int
|
||||
}
|
||||
type E4 struct {
|
||||
E3
|
||||
D int
|
||||
}
|
||||
|
||||
func BenchmarkFieldNameL1(b *testing.B) {
|
||||
e4 := E4{D: 1}
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := reflect.ValueOf(e4)
|
||||
f := v.FieldByName("D")
|
||||
if f.Interface().(int) != 1 {
|
||||
b.Fatal("Wrong value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFieldNameL4(b *testing.B) {
|
||||
e4 := E4{}
|
||||
e4.A = 1
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := reflect.ValueOf(e4)
|
||||
f := v.FieldByName("A")
|
||||
if f.Interface().(int) != 1 {
|
||||
b.Fatal("Wrong value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFieldPosL1(b *testing.B) {
|
||||
e4 := E4{D: 1}
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := reflect.ValueOf(e4)
|
||||
f := v.Field(1)
|
||||
if f.Interface().(int) != 1 {
|
||||
b.Fatal("Wrong value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFieldPosL4(b *testing.B) {
|
||||
e4 := E4{}
|
||||
e4.A = 1
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := reflect.ValueOf(e4)
|
||||
f := v.Field(0)
|
||||
f = f.Field(0)
|
||||
f = f.Field(0)
|
||||
f = f.Field(0)
|
||||
if f.Interface().(int) != 1 {
|
||||
b.Fatal("Wrong value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFieldByIndexL4(b *testing.B) {
|
||||
e4 := E4{}
|
||||
e4.A = 1
|
||||
idx := []int{0, 0, 0, 0}
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := reflect.ValueOf(e4)
|
||||
f := FieldByIndexes(v, idx)
|
||||
if f.Interface().(int) != 1 {
|
||||
b.Fatal("Wrong value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
-1311
File diff suppressed because it is too large
Load Diff
-5
@@ -1,5 +0,0 @@
|
||||
# types
|
||||
|
||||
The types package provides some useful types which implement the `sql.Scanner`
|
||||
and `driver.Valuer` interfaces, suitable for use as scan and value targets with
|
||||
database/sql.
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type GzippedText []byte
|
||||
|
||||
func (g GzippedText) Value() (driver.Value, error) {
|
||||
b := make([]byte, 0, len(g))
|
||||
buf := bytes.NewBuffer(b)
|
||||
w := gzip.NewWriter(buf)
|
||||
w.Write(g)
|
||||
w.Close()
|
||||
return buf.Bytes(), nil
|
||||
|
||||
}
|
||||
|
||||
func (g *GzippedText) Scan(src interface{}) error {
|
||||
var source []byte
|
||||
switch src.(type) {
|
||||
case string:
|
||||
source = []byte(src.(string))
|
||||
case []byte:
|
||||
source = src.([]byte)
|
||||
default:
|
||||
return errors.New("Incompatible type for GzippedText")
|
||||
}
|
||||
reader, err := gzip.NewReader(bytes.NewReader(source))
|
||||
defer reader.Close()
|
||||
b, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*g = GzippedText(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
// JsonText is a json.RawMessage, which is a []byte underneath.
|
||||
// Value() validates the json format in the source, and returns an error if
|
||||
// the json is not valid. Scan does no validation. JsonText additionally
|
||||
// implements `Unmarshal`, which unmarshals the json within to an interface{}
|
||||
type JsonText json.RawMessage
|
||||
|
||||
// Returns the *j as the JSON encoding of j.
|
||||
func (j *JsonText) MarshalJSON() ([]byte, error) {
|
||||
return *j, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON sets *j to a copy of data
|
||||
func (j *JsonText) UnmarshalJSON(data []byte) error {
|
||||
if j == nil {
|
||||
return errors.New("JsonText: UnmarshalJSON on nil pointer")
|
||||
}
|
||||
*j = append((*j)[0:0], data...)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Value returns j as a value. This does a validating unmarshal into another
|
||||
// RawMessage. If j is invalid json, it returns an error.
|
||||
func (j JsonText) Value() (driver.Value, error) {
|
||||
var m json.RawMessage
|
||||
var err = j.Unmarshal(&m)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return []byte(j), nil
|
||||
}
|
||||
|
||||
// Scan stores the src in *j. No validation is done.
|
||||
func (j *JsonText) Scan(src interface{}) error {
|
||||
var source []byte
|
||||
switch src.(type) {
|
||||
case string:
|
||||
source = []byte(src.(string))
|
||||
case []byte:
|
||||
source = src.([]byte)
|
||||
default:
|
||||
return errors.New("Incompatible type for JsonText")
|
||||
}
|
||||
*j = JsonText(append((*j)[0:0], source...))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unmarshal unmarshal's the json in j to v, as in json.Unmarshal.
|
||||
func (j *JsonText) Unmarshal(v interface{}) error {
|
||||
return json.Unmarshal([]byte(*j), v)
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
package types
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGzipText(t *testing.T) {
|
||||
g := GzippedText("Hello, world")
|
||||
v, err := g.Value()
|
||||
if err != nil {
|
||||
t.Errorf("Was not expecting an error")
|
||||
}
|
||||
err = (&g).Scan(v)
|
||||
if err != nil {
|
||||
t.Errorf("Was not expecting an error")
|
||||
}
|
||||
if string(g) != "Hello, world" {
|
||||
t.Errorf("Was expecting the string we sent in (Hello World), got %s", string(g))
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonText(t *testing.T) {
|
||||
j := JsonText(`{"foo": 1, "bar": 2}`)
|
||||
v, err := j.Value()
|
||||
if err != nil {
|
||||
t.Errorf("Was not expecting an error")
|
||||
}
|
||||
err = (&j).Scan(v)
|
||||
if err != nil {
|
||||
t.Errorf("Was not expecting an error")
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
j.Unmarshal(&m)
|
||||
|
||||
if m["foo"].(float64) != 1 || m["bar"].(float64) != 2 {
|
||||
t.Errorf("Expected valid json but got some garbage instead? %#v", m)
|
||||
}
|
||||
|
||||
j = JsonText(`{"foo": 1, invalid, false}`)
|
||||
v, err = j.Value()
|
||||
if err == nil {
|
||||
t.Errorf("Was expecting invalid json to fail!")
|
||||
}
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||
// Based on the path package, Copyright 2009 The Go Authors.
|
||||
// Use of this source code is governed by a BSD-style license that can be found
|
||||
// in the LICENSE file.
|
||||
|
||||
package httprouter
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var cleanTests = []struct {
|
||||
path, result string
|
||||
}{
|
||||
// Already clean
|
||||
{"/", "/"},
|
||||
{"/abc", "/abc"},
|
||||
{"/a/b/c", "/a/b/c"},
|
||||
{"/abc/", "/abc/"},
|
||||
{"/a/b/c/", "/a/b/c/"},
|
||||
|
||||
// missing root
|
||||
{"", "/"},
|
||||
{"abc", "/abc"},
|
||||
{"abc/def", "/abc/def"},
|
||||
{"a/b/c", "/a/b/c"},
|
||||
|
||||
// Remove doubled slash
|
||||
{"//", "/"},
|
||||
{"/abc//", "/abc/"},
|
||||
{"/abc/def//", "/abc/def/"},
|
||||
{"/a/b/c//", "/a/b/c/"},
|
||||
{"/abc//def//ghi", "/abc/def/ghi"},
|
||||
{"//abc", "/abc"},
|
||||
{"///abc", "/abc"},
|
||||
{"//abc//", "/abc/"},
|
||||
|
||||
// Remove . elements
|
||||
{".", "/"},
|
||||
{"./", "/"},
|
||||
{"/abc/./def", "/abc/def"},
|
||||
{"/./abc/def", "/abc/def"},
|
||||
{"/abc/.", "/abc/"},
|
||||
|
||||
// Remove .. elements
|
||||
{"..", "/"},
|
||||
{"../", "/"},
|
||||
{"../../", "/"},
|
||||
{"../..", "/"},
|
||||
{"../../abc", "/abc"},
|
||||
{"/abc/def/ghi/../jkl", "/abc/def/jkl"},
|
||||
{"/abc/def/../ghi/../jkl", "/abc/jkl"},
|
||||
{"/abc/def/..", "/abc"},
|
||||
{"/abc/def/../..", "/"},
|
||||
{"/abc/def/../../..", "/"},
|
||||
{"/abc/def/../../..", "/"},
|
||||
{"/abc/def/../../../ghi/jkl/../../../mno", "/mno"},
|
||||
|
||||
// Combinations
|
||||
{"abc/./../def", "/def"},
|
||||
{"abc//./../def", "/def"},
|
||||
{"abc/../../././../def", "/def"},
|
||||
}
|
||||
|
||||
func TestPathClean(t *testing.T) {
|
||||
for _, test := range cleanTests {
|
||||
if s := CleanPath(test.path); s != test.result {
|
||||
t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result)
|
||||
}
|
||||
if s := CleanPath(test.result); s != test.result {
|
||||
t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathCleanMallocs(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping malloc count in short mode")
|
||||
}
|
||||
if runtime.GOMAXPROCS(0) > 1 {
|
||||
t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
|
||||
return
|
||||
}
|
||||
|
||||
for _, test := range cleanTests {
|
||||
allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) })
|
||||
if allocs > 0 {
|
||||
t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs)
|
||||
}
|
||||
}
|
||||
}
|
||||
-329
@@ -1,329 +0,0 @@
|
||||
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be found
|
||||
// in the LICENSE file.
|
||||
|
||||
package httprouter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockResponseWriter struct{}
|
||||
|
||||
func (m *mockResponseWriter) Header() (h http.Header) {
|
||||
return http.Header{}
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) WriteString(s string) (n int, err error) {
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) WriteHeader(int) {}
|
||||
|
||||
func TestParams(t *testing.T) {
|
||||
ps := Params{
|
||||
Param{"param1", "value1"},
|
||||
Param{"param2", "value2"},
|
||||
Param{"param3", "value3"},
|
||||
}
|
||||
for i := range ps {
|
||||
if val := ps.ByName(ps[i].Key); val != ps[i].Value {
|
||||
t.Errorf("Wrong value for %s: Got %s; Want %s", ps[i].Key, val, ps[i].Value)
|
||||
}
|
||||
}
|
||||
if val := ps.ByName("noKey"); val != "" {
|
||||
t.Errorf("Expected empty string for not found key; got: %s", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter(t *testing.T) {
|
||||
router := New()
|
||||
|
||||
routed := false
|
||||
router.Handle("GET", "/user/:name", func(w http.ResponseWriter, r *http.Request, ps Params) {
|
||||
routed = true
|
||||
want := Params{Param{"name", "gopher"}}
|
||||
if !reflect.DeepEqual(ps, want) {
|
||||
t.Fatalf("wrong wildcard values: want %v, got %v", want, ps)
|
||||
}
|
||||
})
|
||||
|
||||
w := new(mockResponseWriter)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/user/gopher", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if !routed {
|
||||
t.Fatal("routing failed")
|
||||
}
|
||||
}
|
||||
|
||||
type handlerStruct struct {
|
||||
handeled *bool
|
||||
}
|
||||
|
||||
func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
*h.handeled = true
|
||||
}
|
||||
|
||||
func TestRouterAPI(t *testing.T) {
|
||||
var get, post, put, patch, delete, handler, handlerFunc bool
|
||||
|
||||
httpHandler := handlerStruct{&handler}
|
||||
|
||||
router := New()
|
||||
router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
|
||||
get = true
|
||||
})
|
||||
router.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) {
|
||||
post = true
|
||||
})
|
||||
router.PUT("/PUT", func(w http.ResponseWriter, r *http.Request, _ Params) {
|
||||
put = true
|
||||
})
|
||||
router.PATCH("/PATCH", func(w http.ResponseWriter, r *http.Request, _ Params) {
|
||||
patch = true
|
||||
})
|
||||
router.DELETE("/DELETE", func(w http.ResponseWriter, r *http.Request, _ Params) {
|
||||
delete = true
|
||||
})
|
||||
router.Handler("GET", "/Handler", httpHandler)
|
||||
router.HandlerFunc("GET", "/HandlerFunc", func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerFunc = true
|
||||
})
|
||||
|
||||
w := new(mockResponseWriter)
|
||||
|
||||
r, _ := http.NewRequest("GET", "/GET", nil)
|
||||
router.ServeHTTP(w, r)
|
||||
if !get {
|
||||
t.Error("routing GET failed")
|
||||
}
|
||||
|
||||
r, _ = http.NewRequest("POST", "/POST", nil)
|
||||
router.ServeHTTP(w, r)
|
||||
if !post {
|
||||
t.Error("routing POST failed")
|
||||
}
|
||||
|
||||
r, _ = http.NewRequest("PUT", "/PUT", nil)
|
||||
router.ServeHTTP(w, r)
|
||||
if !put {
|
||||
t.Error("routing PUT failed")
|
||||
}
|
||||
|
||||
r, _ = http.NewRequest("PATCH", "/PATCH", nil)
|
||||
router.ServeHTTP(w, r)
|
||||
if !patch {
|
||||
t.Error("routing PATCH failed")
|
||||
}
|
||||
|
||||
r, _ = http.NewRequest("DELETE", "/DELETE", nil)
|
||||
router.ServeHTTP(w, r)
|
||||
if !delete {
|
||||
t.Error("routing DELETE failed")
|
||||
}
|
||||
|
||||
r, _ = http.NewRequest("GET", "/Handler", nil)
|
||||
router.ServeHTTP(w, r)
|
||||
if !handler {
|
||||
t.Error("routing Handler failed")
|
||||
}
|
||||
|
||||
r, _ = http.NewRequest("GET", "/HandlerFunc", nil)
|
||||
router.ServeHTTP(w, r)
|
||||
if !handlerFunc {
|
||||
t.Error("routing HandlerFunc failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterRoot(t *testing.T) {
|
||||
router := New()
|
||||
recv := catchPanic(func() {
|
||||
router.GET("noSlashRoot", nil)
|
||||
})
|
||||
if recv == nil {
|
||||
t.Fatal("registering path not beginning with '/' did not panic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterNotFound(t *testing.T) {
|
||||
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
|
||||
|
||||
router := New()
|
||||
router.GET("/path", handlerFunc)
|
||||
router.GET("/dir/", handlerFunc)
|
||||
|
||||
testRoutes := []struct {
|
||||
route string
|
||||
code int
|
||||
header string
|
||||
}{
|
||||
{"/path/", 301, "map[Location:[/path]]"}, // TSR -/
|
||||
{"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/
|
||||
{"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case
|
||||
{"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case
|
||||
{"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/
|
||||
{"/DIR", 301, "map[Location:[/dir/]]"}, // Fixed Case +/
|
||||
{"/../path", 301, "map[Location:[/path]]"}, // CleanPath
|
||||
{"/nope", 404, ""}, // NotFound
|
||||
}
|
||||
for _, tr := range testRoutes {
|
||||
r, _ := http.NewRequest("GET", tr.route, nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, r)
|
||||
if !(w.Code == tr.code && (w.Code == 404 || fmt.Sprint(w.Header()) == tr.header)) {
|
||||
t.Errorf("NotFound handling route %s failed: Code=%d, Header=%v", tr.route, w.Code, w.Header())
|
||||
}
|
||||
}
|
||||
|
||||
// Test custom not found handler
|
||||
var notFound bool
|
||||
router.NotFound = func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(404)
|
||||
notFound = true
|
||||
}
|
||||
r, _ := http.NewRequest("GET", "/nope", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, r)
|
||||
if !(w.Code == 404 && notFound == true) {
|
||||
t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header())
|
||||
}
|
||||
|
||||
// Test other method than GET (want 307 instead of 301)
|
||||
router.PATCH("/path", handlerFunc)
|
||||
r, _ = http.NewRequest("PATCH", "/path/", nil)
|
||||
w = httptest.NewRecorder()
|
||||
router.ServeHTTP(w, r)
|
||||
if !(w.Code == 307 && fmt.Sprint(w.Header()) == "map[Location:[/path]]") {
|
||||
t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header())
|
||||
}
|
||||
|
||||
// Test special case where no node for the prefix "/" exists
|
||||
router = New()
|
||||
router.GET("/a", handlerFunc)
|
||||
r, _ = http.NewRequest("GET", "/", nil)
|
||||
w = httptest.NewRecorder()
|
||||
router.ServeHTTP(w, r)
|
||||
if !(w.Code == 404) {
|
||||
t.Errorf("NotFound handling route / failed: Code=%d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterPanicHandler(t *testing.T) {
|
||||
router := New()
|
||||
panicHandled := false
|
||||
|
||||
router.PanicHandler = func(rw http.ResponseWriter, r *http.Request, p interface{}) {
|
||||
panicHandled = true
|
||||
}
|
||||
|
||||
router.Handle("PUT", "/user/:name", func(_ http.ResponseWriter, _ *http.Request, _ Params) {
|
||||
panic("oops!")
|
||||
})
|
||||
|
||||
w := new(mockResponseWriter)
|
||||
req, _ := http.NewRequest("PUT", "/user/gopher", nil)
|
||||
|
||||
defer func() {
|
||||
if rcv := recover(); rcv != nil {
|
||||
t.Fatal("handling panic failed")
|
||||
}
|
||||
}()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if !panicHandled {
|
||||
t.Fatal("simulating failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterLookup(t *testing.T) {
|
||||
routed := false
|
||||
wantHandle := func(_ http.ResponseWriter, _ *http.Request, _ Params) {
|
||||
routed = true
|
||||
}
|
||||
wantParams := Params{Param{"name", "gopher"}}
|
||||
|
||||
router := New()
|
||||
|
||||
// try empty router first
|
||||
handle, _, tsr := router.Lookup("GET", "/nope")
|
||||
if handle != nil {
|
||||
t.Fatalf("Got handle for unregistered pattern: %v", handle)
|
||||
}
|
||||
if tsr {
|
||||
t.Error("Got wrong TSR recommendation!")
|
||||
}
|
||||
|
||||
// insert route and try again
|
||||
router.GET("/user/:name", wantHandle)
|
||||
|
||||
handle, params, tsr := router.Lookup("GET", "/user/gopher")
|
||||
if handle == nil {
|
||||
t.Fatal("Got no handle!")
|
||||
} else {
|
||||
handle(nil, nil, nil)
|
||||
if !routed {
|
||||
t.Fatal("Routing failed!")
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(params, wantParams) {
|
||||
t.Fatalf("Wrong parameter values: want %v, got %v", wantParams, params)
|
||||
}
|
||||
|
||||
handle, _, tsr = router.Lookup("GET", "/user/gopher/")
|
||||
if handle != nil {
|
||||
t.Fatalf("Got handle for unregistered pattern: %v", handle)
|
||||
}
|
||||
if !tsr {
|
||||
t.Error("Got no TSR recommendation!")
|
||||
}
|
||||
|
||||
handle, _, tsr = router.Lookup("GET", "/nope")
|
||||
if handle != nil {
|
||||
t.Fatalf("Got handle for unregistered pattern: %v", handle)
|
||||
}
|
||||
if tsr {
|
||||
t.Error("Got wrong TSR recommendation!")
|
||||
}
|
||||
}
|
||||
|
||||
type mockFileSystem struct {
|
||||
opened bool
|
||||
}
|
||||
|
||||
func (mfs *mockFileSystem) Open(name string) (http.File, error) {
|
||||
mfs.opened = true
|
||||
return nil, errors.New("this is just a mock")
|
||||
}
|
||||
|
||||
func TestRouterServeFiles(t *testing.T) {
|
||||
router := New()
|
||||
mfs := &mockFileSystem{}
|
||||
|
||||
recv := catchPanic(func() {
|
||||
router.ServeFiles("/noFilepath", mfs)
|
||||
})
|
||||
if recv == nil {
|
||||
t.Fatal("registering path not ending with '*filepath' did not panic")
|
||||
}
|
||||
|
||||
router.ServeFiles("/*filepath", mfs)
|
||||
w := new(mockResponseWriter)
|
||||
r, _ := http.NewRequest("GET", "/favicon.ico", nil)
|
||||
router.ServeHTTP(w, r)
|
||||
if !mfs.opened {
|
||||
t.Error("serving file failed")
|
||||
}
|
||||
}
|
||||
-559
@@ -1,559 +0,0 @@
|
||||
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be found
|
||||
// in the LICENSE file.
|
||||
|
||||
package httprouter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func printChildren(n *node, prefix string) {
|
||||
fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handle, n.wildChild, n.nType)
|
||||
for l := len(n.path); l > 0; l-- {
|
||||
prefix += " "
|
||||
}
|
||||
for _, child := range n.children {
|
||||
printChildren(child, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// Used as a workaround since we can't compare functions or their adresses
|
||||
var fakeHandlerValue string
|
||||
|
||||
func fakeHandler(val string) Handle {
|
||||
return func(http.ResponseWriter, *http.Request, Params) {
|
||||
fakeHandlerValue = val
|
||||
}
|
||||
}
|
||||
|
||||
type testRequests []struct {
|
||||
path string
|
||||
nilHandler bool
|
||||
route string
|
||||
ps Params
|
||||
}
|
||||
|
||||
func checkRequests(t *testing.T, tree *node, requests testRequests) {
|
||||
for _, request := range requests {
|
||||
handler, ps, _ := tree.getValue(request.path)
|
||||
|
||||
if handler == nil {
|
||||
if !request.nilHandler {
|
||||
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
|
||||
}
|
||||
} else if request.nilHandler {
|
||||
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
|
||||
} else {
|
||||
handler(nil, nil, nil)
|
||||
if fakeHandlerValue != request.route {
|
||||
t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(ps, request.ps) {
|
||||
t.Errorf("Params mismatch for route '%s'", request.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkPriorities(t *testing.T, n *node) uint32 {
|
||||
var prio uint32
|
||||
for i := range n.children {
|
||||
prio += checkPriorities(t, n.children[i])
|
||||
}
|
||||
|
||||
if n.handle != nil {
|
||||
prio++
|
||||
}
|
||||
|
||||
if n.priority != prio {
|
||||
t.Errorf(
|
||||
"priority mismatch for node '%s': is %d, should be %d",
|
||||
n.path, n.priority, prio,
|
||||
)
|
||||
}
|
||||
|
||||
return prio
|
||||
}
|
||||
|
||||
func checkMaxParams(t *testing.T, n *node) uint8 {
|
||||
var maxParams uint8
|
||||
for i := range n.children {
|
||||
params := checkMaxParams(t, n.children[i])
|
||||
if params > maxParams {
|
||||
maxParams = params
|
||||
}
|
||||
}
|
||||
if n.nType != static && !n.wildChild {
|
||||
maxParams++
|
||||
}
|
||||
|
||||
if n.maxParams != maxParams {
|
||||
t.Errorf(
|
||||
"maxParams mismatch for node '%s': is %d, should be %d",
|
||||
n.path, n.maxParams, maxParams,
|
||||
)
|
||||
}
|
||||
|
||||
return maxParams
|
||||
}
|
||||
|
||||
func TestCountParams(t *testing.T) {
|
||||
if countParams("/path/:param1/static/*catch-all") != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
if countParams(strings.Repeat("/:param", 256)) != 255 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeAddAndGet(t *testing.T) {
|
||||
tree := &node{}
|
||||
|
||||
routes := [...]string{
|
||||
"/hi",
|
||||
"/contact",
|
||||
"/co",
|
||||
"/c",
|
||||
"/a",
|
||||
"/ab",
|
||||
"/doc/",
|
||||
"/doc/go_faq.html",
|
||||
"/doc/go1.html",
|
||||
}
|
||||
for _, route := range routes {
|
||||
tree.addRoute(route, fakeHandler(route))
|
||||
}
|
||||
|
||||
//printChildren(tree, "")
|
||||
|
||||
checkRequests(t, tree, testRequests{
|
||||
{"/a", false, "/a", nil},
|
||||
{"/", true, "", nil},
|
||||
{"/hi", false, "/hi", nil},
|
||||
{"/contact", false, "/contact", nil},
|
||||
{"/co", false, "/co", nil},
|
||||
{"/con", true, "", nil}, // key mismatch
|
||||
{"/cona", true, "", nil}, // key mismatch
|
||||
{"/no", true, "", nil}, // no matching child
|
||||
{"/ab", false, "/ab", nil},
|
||||
})
|
||||
|
||||
checkPriorities(t, tree)
|
||||
checkMaxParams(t, tree)
|
||||
}
|
||||
|
||||
func TestTreeWildcard(t *testing.T) {
|
||||
tree := &node{}
|
||||
|
||||
routes := [...]string{
|
||||
"/",
|
||||
"/cmd/:tool/:sub",
|
||||
"/cmd/:tool/",
|
||||
"/src/*filepath",
|
||||
"/search/",
|
||||
"/search/:query",
|
||||
"/user_:name",
|
||||
"/user_:name/about",
|
||||
"/files/:dir/*filepath",
|
||||
"/doc/",
|
||||
"/doc/go_faq.html",
|
||||
"/doc/go1.html",
|
||||
"/info/:user/public",
|
||||
"/info/:user/project/:project",
|
||||
}
|
||||
for _, route := range routes {
|
||||
tree.addRoute(route, fakeHandler(route))
|
||||
}
|
||||
|
||||
//printChildren(tree, "")
|
||||
|
||||
checkRequests(t, tree, testRequests{
|
||||
{"/", false, "/", nil},
|
||||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
||||
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
|
||||
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
|
||||
{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
|
||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
||||
{"/search/", false, "/search/", nil},
|
||||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
||||
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
||||
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
|
||||
{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}},
|
||||
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
|
||||
{"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}},
|
||||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
|
||||
})
|
||||
|
||||
checkPriorities(t, tree)
|
||||
checkMaxParams(t, tree)
|
||||
}
|
||||
|
||||
func catchPanic(testFunc func()) (recv interface{}) {
|
||||
defer func() {
|
||||
recv = recover()
|
||||
}()
|
||||
|
||||
testFunc()
|
||||
return
|
||||
}
|
||||
|
||||
type testRoute struct {
|
||||
path string
|
||||
conflict bool
|
||||
}
|
||||
|
||||
func testRoutes(t *testing.T, routes []testRoute) {
|
||||
tree := &node{}
|
||||
|
||||
for _, route := range routes {
|
||||
recv := catchPanic(func() {
|
||||
tree.addRoute(route.path, nil)
|
||||
})
|
||||
|
||||
if route.conflict {
|
||||
if recv == nil {
|
||||
t.Errorf("no panic for conflicting route '%s'", route.path)
|
||||
}
|
||||
} else if recv != nil {
|
||||
t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
|
||||
}
|
||||
}
|
||||
|
||||
//printChildren(tree, "")
|
||||
}
|
||||
|
||||
func TestTreeWildcardConflict(t *testing.T) {
|
||||
routes := []testRoute{
|
||||
{"/cmd/:tool/:sub", false},
|
||||
{"/cmd/vet", true},
|
||||
{"/src/*filepath", false},
|
||||
{"/src/*filepathx", true},
|
||||
{"/src/", true},
|
||||
{"/src1/", false},
|
||||
{"/src1/*filepath", true},
|
||||
{"/src2*filepath", true},
|
||||
{"/search/:query", false},
|
||||
{"/search/invalid", true},
|
||||
{"/user_:name", false},
|
||||
{"/user_x", true},
|
||||
{"/user_:name", false},
|
||||
{"/id:id", false},
|
||||
{"/id/:id", true},
|
||||
}
|
||||
testRoutes(t, routes)
|
||||
}
|
||||
|
||||
func TestTreeChildConflict(t *testing.T) {
|
||||
routes := []testRoute{
|
||||
{"/cmd/vet", false},
|
||||
{"/cmd/:tool/:sub", true},
|
||||
{"/src/AUTHORS", false},
|
||||
{"/src/*filepath", true},
|
||||
{"/user_x", false},
|
||||
{"/user_:name", true},
|
||||
{"/id/:id", false},
|
||||
{"/id:id", true},
|
||||
{"/:id", true},
|
||||
{"/*filepath", true},
|
||||
}
|
||||
testRoutes(t, routes)
|
||||
}
|
||||
|
||||
func TestTreeDupliatePath(t *testing.T) {
|
||||
tree := &node{}
|
||||
|
||||
routes := [...]string{
|
||||
"/",
|
||||
"/doc/",
|
||||
"/src/*filepath",
|
||||
"/search/:query",
|
||||
"/user_:name",
|
||||
}
|
||||
for _, route := range routes {
|
||||
recv := catchPanic(func() {
|
||||
tree.addRoute(route, fakeHandler(route))
|
||||
})
|
||||
if recv != nil {
|
||||
t.Fatalf("panic inserting route '%s': %v", route, recv)
|
||||
}
|
||||
|
||||
// Add again
|
||||
recv = catchPanic(func() {
|
||||
tree.addRoute(route, nil)
|
||||
})
|
||||
if recv == nil {
|
||||
t.Fatalf("no panic while inserting duplicate route '%s", route)
|
||||
}
|
||||
}
|
||||
|
||||
//printChildren(tree, "")
|
||||
|
||||
checkRequests(t, tree, testRequests{
|
||||
{"/", false, "/", nil},
|
||||
{"/doc/", false, "/doc/", nil},
|
||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
||||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
||||
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestEmptyWildcardName(t *testing.T) {
|
||||
tree := &node{}
|
||||
|
||||
routes := [...]string{
|
||||
"/user:",
|
||||
"/user:/",
|
||||
"/cmd/:/",
|
||||
"/src/*",
|
||||
}
|
||||
for _, route := range routes {
|
||||
recv := catchPanic(func() {
|
||||
tree.addRoute(route, nil)
|
||||
})
|
||||
if recv == nil {
|
||||
t.Fatalf("no panic while inserting route with empty wildcard name '%s", route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeCatchAllConflict(t *testing.T) {
|
||||
routes := []testRoute{
|
||||
{"/src/*filepath/x", true},
|
||||
{"/src2/", false},
|
||||
{"/src2/*filepath/x", true},
|
||||
}
|
||||
testRoutes(t, routes)
|
||||
}
|
||||
|
||||
func TestTreeCatchAllConflictRoot(t *testing.T) {
|
||||
routes := []testRoute{
|
||||
{"/", false},
|
||||
{"/*filepath", true},
|
||||
}
|
||||
testRoutes(t, routes)
|
||||
}
|
||||
|
||||
/*func TestTreeDuplicateWildcard(t *testing.T) {
|
||||
tree := &node{}
|
||||
|
||||
routes := [...]string{
|
||||
"/:id/:name/:id",
|
||||
}
|
||||
for _, route := range routes {
|
||||
...
|
||||
}
|
||||
}*/
|
||||
|
||||
func TestTreeTrailingSlashRedirect(t *testing.T) {
|
||||
tree := &node{}
|
||||
|
||||
routes := [...]string{
|
||||
"/hi",
|
||||
"/b/",
|
||||
"/search/:query",
|
||||
"/cmd/:tool/",
|
||||
"/src/*filepath",
|
||||
"/x",
|
||||
"/x/y",
|
||||
"/y/",
|
||||
"/y/z",
|
||||
"/0/:id",
|
||||
"/0/:id/1",
|
||||
"/1/:id/",
|
||||
"/1/:id/2",
|
||||
"/aa",
|
||||
"/a/",
|
||||
"/doc",
|
||||
"/doc/go_faq.html",
|
||||
"/doc/go1.html",
|
||||
"/no/a",
|
||||
"/no/b",
|
||||
"/api/hello/:name",
|
||||
}
|
||||
for _, route := range routes {
|
||||
recv := catchPanic(func() {
|
||||
tree.addRoute(route, fakeHandler(route))
|
||||
})
|
||||
if recv != nil {
|
||||
t.Fatalf("panic inserting route '%s': %v", route, recv)
|
||||
}
|
||||
}
|
||||
|
||||
//printChildren(tree, "")
|
||||
|
||||
tsrRoutes := [...]string{
|
||||
"/hi/",
|
||||
"/b",
|
||||
"/search/gopher/",
|
||||
"/cmd/vet",
|
||||
"/src",
|
||||
"/x/",
|
||||
"/y",
|
||||
"/0/go/",
|
||||
"/1/go",
|
||||
"/a",
|
||||
"/doc/",
|
||||
}
|
||||
for _, route := range tsrRoutes {
|
||||
handler, _, tsr := tree.getValue(route)
|
||||
if handler != nil {
|
||||
t.Fatalf("non-nil handler for TSR route '%s", route)
|
||||
} else if !tsr {
|
||||
t.Errorf("expected TSR recommendation for route '%s'", route)
|
||||
}
|
||||
}
|
||||
|
||||
noTsrRoutes := [...]string{
|
||||
"/",
|
||||
"/no",
|
||||
"/no/",
|
||||
"/_",
|
||||
"/_/",
|
||||
"/api/world/abc",
|
||||
}
|
||||
for _, route := range noTsrRoutes {
|
||||
handler, _, tsr := tree.getValue(route)
|
||||
if handler != nil {
|
||||
t.Fatalf("non-nil handler for No-TSR route '%s", route)
|
||||
} else if tsr {
|
||||
t.Errorf("expected no TSR recommendation for route '%s'", route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeFindCaseInsensitivePath(t *testing.T) {
|
||||
tree := &node{}
|
||||
|
||||
routes := [...]string{
|
||||
"/hi",
|
||||
"/b/",
|
||||
"/ABC/",
|
||||
"/search/:query",
|
||||
"/cmd/:tool/",
|
||||
"/src/*filepath",
|
||||
"/x",
|
||||
"/x/y",
|
||||
"/y/",
|
||||
"/y/z",
|
||||
"/0/:id",
|
||||
"/0/:id/1",
|
||||
"/1/:id/",
|
||||
"/1/:id/2",
|
||||
"/aa",
|
||||
"/a/",
|
||||
"/doc",
|
||||
"/doc/go_faq.html",
|
||||
"/doc/go1.html",
|
||||
"/doc/go/away",
|
||||
"/no/a",
|
||||
"/no/b",
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
recv := catchPanic(func() {
|
||||
tree.addRoute(route, fakeHandler(route))
|
||||
})
|
||||
if recv != nil {
|
||||
t.Fatalf("panic inserting route '%s': %v", route, recv)
|
||||
}
|
||||
}
|
||||
|
||||
// Check out == in for all registered routes
|
||||
// With fixTrailingSlash = true
|
||||
for _, route := range routes {
|
||||
out, found := tree.findCaseInsensitivePath(route, true)
|
||||
if !found {
|
||||
t.Errorf("Route '%s' not found!", route)
|
||||
} else if string(out) != route {
|
||||
t.Errorf("Wrong result for route '%s': %s", route, string(out))
|
||||
}
|
||||
}
|
||||
// With fixTrailingSlash = false
|
||||
for _, route := range routes {
|
||||
out, found := tree.findCaseInsensitivePath(route, false)
|
||||
if !found {
|
||||
t.Errorf("Route '%s' not found!", route)
|
||||
} else if string(out) != route {
|
||||
t.Errorf("Wrong result for route '%s': %s", route, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
in string
|
||||
out string
|
||||
found bool
|
||||
slash bool
|
||||
}{
|
||||
{"/HI", "/hi", true, false},
|
||||
{"/HI/", "/hi", true, true},
|
||||
{"/B", "/b/", true, true},
|
||||
{"/B/", "/b/", true, false},
|
||||
{"/abc", "/ABC/", true, true},
|
||||
{"/abc/", "/ABC/", true, false},
|
||||
{"/aBc", "/ABC/", true, true},
|
||||
{"/aBc/", "/ABC/", true, false},
|
||||
{"/abC", "/ABC/", true, true},
|
||||
{"/abC/", "/ABC/", true, false},
|
||||
{"/SEARCH/QUERY", "/search/QUERY", true, false},
|
||||
{"/SEARCH/QUERY/", "/search/QUERY", true, true},
|
||||
{"/CMD/TOOL/", "/cmd/TOOL/", true, false},
|
||||
{"/CMD/TOOL", "/cmd/TOOL/", true, true},
|
||||
{"/SRC/FILE/PATH", "/src/FILE/PATH", true, false},
|
||||
{"/x/Y", "/x/y", true, false},
|
||||
{"/x/Y/", "/x/y", true, true},
|
||||
{"/X/y", "/x/y", true, false},
|
||||
{"/X/y/", "/x/y", true, true},
|
||||
{"/X/Y", "/x/y", true, false},
|
||||
{"/X/Y/", "/x/y", true, true},
|
||||
{"/Y/", "/y/", true, false},
|
||||
{"/Y", "/y/", true, true},
|
||||
{"/Y/z", "/y/z", true, false},
|
||||
{"/Y/z/", "/y/z", true, true},
|
||||
{"/Y/Z", "/y/z", true, false},
|
||||
{"/Y/Z/", "/y/z", true, true},
|
||||
{"/y/Z", "/y/z", true, false},
|
||||
{"/y/Z/", "/y/z", true, true},
|
||||
{"/Aa", "/aa", true, false},
|
||||
{"/Aa/", "/aa", true, true},
|
||||
{"/AA", "/aa", true, false},
|
||||
{"/AA/", "/aa", true, true},
|
||||
{"/aA", "/aa", true, false},
|
||||
{"/aA/", "/aa", true, true},
|
||||
{"/A/", "/a/", true, false},
|
||||
{"/A", "/a/", true, true},
|
||||
{"/DOC", "/doc", true, false},
|
||||
{"/DOC/", "/doc", true, true},
|
||||
{"/NO", "", false, true},
|
||||
{"/DOC/GO", "", false, true},
|
||||
}
|
||||
// With fixTrailingSlash = true
|
||||
for _, test := range tests {
|
||||
out, found := tree.findCaseInsensitivePath(test.in, true)
|
||||
if found != test.found || (found && (string(out) != test.out)) {
|
||||
t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
|
||||
test.in, string(out), found, test.out, test.found)
|
||||
return
|
||||
}
|
||||
}
|
||||
// With fixTrailingSlash = false
|
||||
for _, test := range tests {
|
||||
out, found := tree.findCaseInsensitivePath(test.in, false)
|
||||
if test.slash {
|
||||
if found { // test needs a trailingSlash fix. It must not be found!
|
||||
t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out))
|
||||
}
|
||||
} else {
|
||||
if found != test.found || (found && (string(out) != test.out)) {
|
||||
t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
|
||||
test.in, string(out), found, test.out, test.found)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-436
@@ -1,436 +0,0 @@
|
||||
// +build go1.1
|
||||
|
||||
package pq
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"github.com/lib/pq/oid"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
selectStringQuery = "SELECT '" + strings.Repeat("0123456789", 10) + "'"
|
||||
selectSeriesQuery = "SELECT generate_series(1, 100)"
|
||||
)
|
||||
|
||||
func BenchmarkSelectString(b *testing.B) {
|
||||
var result string
|
||||
benchQuery(b, selectStringQuery, &result)
|
||||
}
|
||||
|
||||
func BenchmarkSelectSeries(b *testing.B) {
|
||||
var result int
|
||||
benchQuery(b, selectSeriesQuery, &result)
|
||||
}
|
||||
|
||||
func benchQuery(b *testing.B, query string, result interface{}) {
|
||||
b.Skip("current pq database-backed benchmarks are inconsistent")
|
||||
b.StopTimer()
|
||||
db := openTestConn(b)
|
||||
defer db.Close()
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchQueryLoop(b, db, query, result)
|
||||
}
|
||||
}
|
||||
|
||||
func benchQueryLoop(b *testing.B, db *sql.DB, query string, result interface{}) {
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
err = rows.Scan(result)
|
||||
if err != nil {
|
||||
b.Fatal("failed to scan", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reading from circularConn yields content[:prefixLen] once, followed by
|
||||
// content[prefixLen:] over and over again. It never returns EOF.
|
||||
type circularConn struct {
|
||||
content string
|
||||
prefixLen int
|
||||
pos int
|
||||
net.Conn // for all other net.Conn methods that will never be called
|
||||
}
|
||||
|
||||
func (r *circularConn) Read(b []byte) (n int, err error) {
|
||||
n = copy(b, r.content[r.pos:])
|
||||
r.pos += n
|
||||
if r.pos >= len(r.content) {
|
||||
r.pos = r.prefixLen
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *circularConn) Write(b []byte) (n int, err error) { return len(b), nil }
|
||||
|
||||
func (r *circularConn) Close() error { return nil }
|
||||
|
||||
func fakeConn(content string, prefixLen int) *conn {
|
||||
c := &circularConn{content: content, prefixLen: prefixLen}
|
||||
return &conn{buf: bufio.NewReader(c), c: c}
|
||||
}
|
||||
|
||||
// This benchmark is meant to be the same as BenchmarkSelectString, but takes
|
||||
// out some of the factors this package can't control. The numbers are less noisy,
|
||||
// but also the costs of network communication aren't accurately represented.
|
||||
func BenchmarkMockSelectString(b *testing.B) {
|
||||
b.StopTimer()
|
||||
// taken from a recorded run of BenchmarkSelectString
|
||||
// See: http://www.postgresql.org/docs/current/static/protocol-message-formats.html
|
||||
const response = "1\x00\x00\x00\x04" +
|
||||
"t\x00\x00\x00\x06\x00\x00" +
|
||||
"T\x00\x00\x00!\x00\x01?column?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc1\xff\xfe\xff\xff\xff\xff\x00\x00" +
|
||||
"Z\x00\x00\x00\x05I" +
|
||||
"2\x00\x00\x00\x04" +
|
||||
"D\x00\x00\x00n\x00\x01\x00\x00\x00d0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" +
|
||||
"C\x00\x00\x00\rSELECT 1\x00" +
|
||||
"Z\x00\x00\x00\x05I" +
|
||||
"3\x00\x00\x00\x04" +
|
||||
"Z\x00\x00\x00\x05I"
|
||||
c := fakeConn(response, 0)
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchMockQuery(b, c, selectStringQuery)
|
||||
}
|
||||
}
|
||||
|
||||
var seriesRowData = func() string {
|
||||
var buf bytes.Buffer
|
||||
for i := 1; i <= 100; i++ {
|
||||
digits := byte(2)
|
||||
if i >= 100 {
|
||||
digits = 3
|
||||
} else if i < 10 {
|
||||
digits = 1
|
||||
}
|
||||
buf.WriteString("D\x00\x00\x00")
|
||||
buf.WriteByte(10 + digits)
|
||||
buf.WriteString("\x00\x01\x00\x00\x00")
|
||||
buf.WriteByte(digits)
|
||||
buf.WriteString(strconv.Itoa(i))
|
||||
}
|
||||
return buf.String()
|
||||
}()
|
||||
|
||||
func BenchmarkMockSelectSeries(b *testing.B) {
|
||||
b.StopTimer()
|
||||
var response = "1\x00\x00\x00\x04" +
|
||||
"t\x00\x00\x00\x06\x00\x00" +
|
||||
"T\x00\x00\x00!\x00\x01?column?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc1\xff\xfe\xff\xff\xff\xff\x00\x00" +
|
||||
"Z\x00\x00\x00\x05I" +
|
||||
"2\x00\x00\x00\x04" +
|
||||
seriesRowData +
|
||||
"C\x00\x00\x00\x0fSELECT 100\x00" +
|
||||
"Z\x00\x00\x00\x05I" +
|
||||
"3\x00\x00\x00\x04" +
|
||||
"Z\x00\x00\x00\x05I"
|
||||
c := fakeConn(response, 0)
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchMockQuery(b, c, selectSeriesQuery)
|
||||
}
|
||||
}
|
||||
|
||||
func benchMockQuery(b *testing.B, c *conn, query string) {
|
||||
stmt, err := c.Prepare(query)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.Query(nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var dest [1]driver.Value
|
||||
for {
|
||||
if err := rows.Next(dest[:]); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPreparedSelectString(b *testing.B) {
|
||||
var result string
|
||||
benchPreparedQuery(b, selectStringQuery, &result)
|
||||
}
|
||||
|
||||
func BenchmarkPreparedSelectSeries(b *testing.B) {
|
||||
var result int
|
||||
benchPreparedQuery(b, selectSeriesQuery, &result)
|
||||
}
|
||||
|
||||
func benchPreparedQuery(b *testing.B, query string, result interface{}) {
|
||||
b.Skip("current pq database-backed benchmarks are inconsistent")
|
||||
b.StopTimer()
|
||||
db := openTestConn(b)
|
||||
defer db.Close()
|
||||
stmt, err := db.Prepare(query)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchPreparedQueryLoop(b, db, stmt, result)
|
||||
}
|
||||
}
|
||||
|
||||
func benchPreparedQueryLoop(b *testing.B, db *sql.DB, stmt *sql.Stmt, result interface{}) {
|
||||
rows, err := stmt.Query()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if !rows.Next() {
|
||||
rows.Close()
|
||||
b.Fatal("no rows")
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&result)
|
||||
if err != nil {
|
||||
b.Fatal("failed to scan")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See the comment for BenchmarkMockSelectString.
|
||||
func BenchmarkMockPreparedSelectString(b *testing.B) {
|
||||
b.StopTimer()
|
||||
const parseResponse = "1\x00\x00\x00\x04" +
|
||||
"t\x00\x00\x00\x06\x00\x00" +
|
||||
"T\x00\x00\x00!\x00\x01?column?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc1\xff\xfe\xff\xff\xff\xff\x00\x00" +
|
||||
"Z\x00\x00\x00\x05I"
|
||||
const responses = parseResponse +
|
||||
"2\x00\x00\x00\x04" +
|
||||
"D\x00\x00\x00n\x00\x01\x00\x00\x00d0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" +
|
||||
"C\x00\x00\x00\rSELECT 1\x00" +
|
||||
"Z\x00\x00\x00\x05I"
|
||||
c := fakeConn(responses, len(parseResponse))
|
||||
|
||||
stmt, err := c.Prepare(selectStringQuery)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchPreparedMockQuery(b, c, stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMockPreparedSelectSeries(b *testing.B) {
|
||||
b.StopTimer()
|
||||
const parseResponse = "1\x00\x00\x00\x04" +
|
||||
"t\x00\x00\x00\x06\x00\x00" +
|
||||
"T\x00\x00\x00!\x00\x01?column?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc1\xff\xfe\xff\xff\xff\xff\x00\x00" +
|
||||
"Z\x00\x00\x00\x05I"
|
||||
var responses = parseResponse +
|
||||
"2\x00\x00\x00\x04" +
|
||||
seriesRowData +
|
||||
"C\x00\x00\x00\x0fSELECT 100\x00" +
|
||||
"Z\x00\x00\x00\x05I"
|
||||
c := fakeConn(responses, len(parseResponse))
|
||||
|
||||
stmt, err := c.Prepare(selectSeriesQuery)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchPreparedMockQuery(b, c, stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func benchPreparedMockQuery(b *testing.B, c *conn, stmt driver.Stmt) {
|
||||
rows, err := stmt.Query(nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var dest [1]driver.Value
|
||||
for {
|
||||
if err := rows.Next(dest[:]); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodeInt64(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
encode(¶meterStatus{}, int64(1234), oid.T_int8)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodeFloat64(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
encode(¶meterStatus{}, 3.14159, oid.T_float8)
|
||||
}
|
||||
}
|
||||
|
||||
var testByteString = []byte("abcdefghijklmnopqrstuvwxyz")
|
||||
|
||||
func BenchmarkEncodeByteaHex(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
encode(¶meterStatus{serverVersion: 90000}, testByteString, oid.T_bytea)
|
||||
}
|
||||
}
|
||||
func BenchmarkEncodeByteaEscape(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
encode(¶meterStatus{serverVersion: 84000}, testByteString, oid.T_bytea)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodeBool(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
encode(¶meterStatus{}, true, oid.T_bool)
|
||||
}
|
||||
}
|
||||
|
||||
var testTimestamptz = time.Date(2001, time.January, 1, 0, 0, 0, 0, time.Local)
|
||||
|
||||
func BenchmarkEncodeTimestamptz(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
encode(¶meterStatus{}, testTimestamptz, oid.T_timestamptz)
|
||||
}
|
||||
}
|
||||
|
||||
var testIntBytes = []byte("1234")
|
||||
|
||||
func BenchmarkDecodeInt64(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
decode(¶meterStatus{}, testIntBytes, oid.T_int8)
|
||||
}
|
||||
}
|
||||
|
||||
var testFloatBytes = []byte("3.14159")
|
||||
|
||||
func BenchmarkDecodeFloat64(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
decode(¶meterStatus{}, testFloatBytes, oid.T_float8)
|
||||
}
|
||||
}
|
||||
|
||||
var testBoolBytes = []byte{'t'}
|
||||
|
||||
func BenchmarkDecodeBool(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
decode(¶meterStatus{}, testBoolBytes, oid.T_bool)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeBool(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
rows, err := db.Query("select true")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
var testTimestamptzBytes = []byte("2013-09-17 22:15:32.360754-07")
|
||||
|
||||
func BenchmarkDecodeTimestamptz(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
decode(¶meterStatus{}, testTimestamptzBytes, oid.T_timestamptz)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecodeTimestamptzMultiThread(b *testing.B) {
|
||||
oldProcs := runtime.GOMAXPROCS(0)
|
||||
defer runtime.GOMAXPROCS(oldProcs)
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
globalLocationCache = newLocationCache()
|
||||
|
||||
f := func(wg *sync.WaitGroup, loops int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < loops; i++ {
|
||||
decode(¶meterStatus{}, testTimestamptzBytes, oid.T_timestamptz)
|
||||
}
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
b.ResetTimer()
|
||||
for j := 0; j < 10; j++ {
|
||||
wg.Add(1)
|
||||
go f(wg, b.N/10)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkLocationCache(b *testing.B) {
|
||||
globalLocationCache = newLocationCache()
|
||||
for i := 0; i < b.N; i++ {
|
||||
globalLocationCache.getLocation(rand.Intn(10000))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLocationCacheMultiThread(b *testing.B) {
|
||||
oldProcs := runtime.GOMAXPROCS(0)
|
||||
defer runtime.GOMAXPROCS(oldProcs)
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
globalLocationCache = newLocationCache()
|
||||
|
||||
f := func(wg *sync.WaitGroup, loops int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < loops; i++ {
|
||||
globalLocationCache.getLocation(rand.Intn(10000))
|
||||
}
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
b.ResetTimer()
|
||||
for j := 0; j < 10; j++ {
|
||||
wg.Add(1)
|
||||
go f(wg, b.N/10)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Stress test the performance of parsing results from the wire.
|
||||
func BenchmarkResultParsing(b *testing.B) {
|
||||
b.StopTimer()
|
||||
|
||||
db := openTestConn(b)
|
||||
defer db.Close()
|
||||
_, err := db.Exec("BEGIN")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
res, err := db.Query("SELECT generate_series(1, 50000)")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
res.Close()
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
This directory contains certificates and private keys for testing some
|
||||
SSL-related functionality in Travis. Do NOT use these certificates for
|
||||
anything other than testing.
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 2 (0x2)
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: C=US, ST=Nevada, L=Las Vegas, O=github.com/lib/pq, CN=pq CA
|
||||
Validity
|
||||
Not Before: Oct 11 15:10:11 2014 GMT
|
||||
Not After : Oct 8 15:10:11 2024 GMT
|
||||
Subject: C=US, ST=Nevada, L=Las Vegas, O=github.com/lib/pq, CN=pqgosslcert
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
RSA Public Key: (1024 bit)
|
||||
Modulus (1024 bit):
|
||||
00:e3:8c:06:9a:70:54:51:d1:34:34:83:39:cd:a2:
|
||||
59:0f:05:ed:8d:d8:0e:34:d0:92:f4:09:4d:ee:8c:
|
||||
78:55:49:24:f8:3c:e0:34:58:02:b2:e7:94:58:c1:
|
||||
e8:e5:bb:d1:af:f6:54:c1:40:b1:90:70:79:0d:35:
|
||||
54:9c:8f:16:e9:c2:f0:92:e6:64:49:38:c1:76:f8:
|
||||
47:66:c4:5b:4a:b6:a9:43:ce:c8:be:6c:4d:2b:94:
|
||||
97:3c:55:bc:d1:d0:6e:b7:53:ae:89:5c:4b:6b:86:
|
||||
40:be:c1:ae:1e:64:ce:9c:ae:87:0a:69:e5:c8:21:
|
||||
12:be:ae:1d:f6:45:df:16:a7
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Subject Key Identifier:
|
||||
9B:25:31:63:A2:D8:06:FF:CB:E3:E9:96:FF:0D:BA:DC:12:7D:04:CF
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:52:93:ED:1E:76:0A:9F:65:4F:DE:19:66:C1:D5:22:40:35:CB:A0:72
|
||||
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
X509v3 Key Usage:
|
||||
Digital Signature, Non Repudiation, Key Encipherment
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
3e:f5:f8:0b:4e:11:bd:00:86:1f:ce:dc:97:02:98:91:11:f5:
|
||||
65:f6:f2:8a:b2:3e:47:92:05:69:28:c9:e9:b4:f7:cf:93:d1:
|
||||
2d:81:5d:00:3c:23:be:da:70:ea:59:e1:2c:d3:25:49:ae:a6:
|
||||
95:54:c1:10:df:23:e3:fe:d6:e4:76:c7:6b:73:ad:1b:34:7c:
|
||||
e2:56:cc:c0:37:ae:c5:7a:11:20:6c:3d:05:0e:99:cd:22:6c:
|
||||
cf:59:a1:da:28:d4:65:ba:7d:2f:2b:3d:69:6d:a6:c1:ae:57:
|
||||
bf:56:64:13:79:f8:48:46:65:eb:81:67:28:0b:7b:de:47:10:
|
||||
b3:80:3c:31:d1:58:94:01:51:4a:c7:c8:1a:01:a8:af:c4:cd:
|
||||
bb:84:a5:d9:8b:b4:b9:a1:64:3e:95:d9:90:1d:d5:3f:67:cc:
|
||||
3b:ba:f5:b4:d1:33:77:ee:c2:d2:3e:7e:c5:66:6e:b7:35:4c:
|
||||
60:57:b0:b8:be:36:c8:f3:d3:95:8c:28:4a:c9:f7:27:a4:0d:
|
||||
e5:96:99:eb:f5:c8:bd:f3:84:6d:ef:02:f9:8a:36:7d:6b:5f:
|
||||
36:68:37:41:d9:74:ae:c6:78:2e:44:86:a1:ad:43:ca:fb:b5:
|
||||
3e:ba:10:23:09:02:ac:62:d1:d0:83:c8:95:b9:e3:5e:30:ff:
|
||||
5b:2b:38:fa
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDEzCCAfugAwIBAgIBAjANBgkqhkiG9w0BAQsFADBeMQswCQYDVQQGEwJVUzEP
|
||||
MA0GA1UECBMGTmV2YWRhMRIwEAYDVQQHEwlMYXMgVmVnYXMxGjAYBgNVBAoTEWdp
|
||||
dGh1Yi5jb20vbGliL3BxMQ4wDAYDVQQDEwVwcSBDQTAeFw0xNDEwMTExNTEwMTFa
|
||||
Fw0yNDEwMDgxNTEwMTFaMGQxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIEwZOZXZhZGEx
|
||||
EjAQBgNVBAcTCUxhcyBWZWdhczEaMBgGA1UEChMRZ2l0aHViLmNvbS9saWIvcHEx
|
||||
FDASBgNVBAMTC3BxZ29zc2xjZXJ0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
|
||||
gQDjjAaacFRR0TQ0gznNolkPBe2N2A400JL0CU3ujHhVSST4POA0WAKy55RYwejl
|
||||
u9Gv9lTBQLGQcHkNNVScjxbpwvCS5mRJOMF2+EdmxFtKtqlDzsi+bE0rlJc8VbzR
|
||||
0G63U66JXEtrhkC+wa4eZM6crocKaeXIIRK+rh32Rd8WpwIDAQABo1owWDAdBgNV
|
||||
HQ4EFgQUmyUxY6LYBv/L4+mW/w263BJ9BM8wHwYDVR0jBBgwFoAUUpPtHnYKn2VP
|
||||
3hlmwdUiQDXLoHIwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQEL
|
||||
BQADggEBAD71+AtOEb0Ahh/O3JcCmJER9WX28oqyPkeSBWkoyem098+T0S2BXQA8
|
||||
I77acOpZ4SzTJUmuppVUwRDfI+P+1uR2x2tzrRs0fOJWzMA3rsV6ESBsPQUOmc0i
|
||||
bM9Zodoo1GW6fS8rPWltpsGuV79WZBN5+EhGZeuBZygLe95HELOAPDHRWJQBUUrH
|
||||
yBoBqK/EzbuEpdmLtLmhZD6V2ZAd1T9nzDu69bTRM3fuwtI+fsVmbrc1TGBXsLi+
|
||||
Nsjz05WMKErJ9yekDeWWmev1yL3zhG3vAvmKNn1rXzZoN0HZdK7GeC5EhqGtQ8r7
|
||||
tT66ECMJAqxi0dCDyJW5414w/1srOPo=
|
||||
-----END CERTIFICATE-----
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICWwIBAAKBgQDjjAaacFRR0TQ0gznNolkPBe2N2A400JL0CU3ujHhVSST4POA0
|
||||
WAKy55RYwejlu9Gv9lTBQLGQcHkNNVScjxbpwvCS5mRJOMF2+EdmxFtKtqlDzsi+
|
||||
bE0rlJc8VbzR0G63U66JXEtrhkC+wa4eZM6crocKaeXIIRK+rh32Rd8WpwIDAQAB
|
||||
AoGAM5dM6/kp9P700i8qjOgRPym96Zoh5nGfz/rIE5z/r36NBkdvIg8OVZfR96nH
|
||||
b0b9TOMR5lsPp0sI9yivTWvX6qyvLJRWy2vvx17hXK9NxXUNTAm0PYZUTvCtcPeX
|
||||
RnJpzQKNZQPkFzF0uXBc4CtPK2Vz0+FGvAelrhYAxnw1dIkCQQD+9qaW5QhXjsjb
|
||||
Nl85CmXgxPmGROcgLQCO+omfrjf9UXrituU9Dz6auym5lDGEdMFnkzfr+wpasEy9
|
||||
mf5ZZOhDAkEA5HjXfVGaCtpydOt6hDon/uZsyssCK2lQ7NSuE3vP+sUsYMzIpEoy
|
||||
t3VWXqKbo+g9KNDTP4WEliqp1aiSIylzzQJANPeqzihQnlgEdD4MdD4rwhFJwVIp
|
||||
Le8Lcais1KaN7StzOwxB/XhgSibd2TbnPpw+3bSg5n5lvUdo+e62/31OHwJAU1jS
|
||||
I+F09KikQIr28u3UUWT2IzTT4cpVv1AHAQyV3sG3YsjSGT0IK20eyP9BEBZU2WL0
|
||||
7aNjrvR5aHxKc5FXsQJABsFtyGpgI5X4xufkJZVZ+Mklz2n7iXa+XPatMAHFxAtb
|
||||
EEMt60rngwMjXAzBSC6OYuYogRRAY3UCacNC5VhLYQ==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEAzCCAuugAwIBAgIJANmheROCdW1NMA0GCSqGSIb3DQEBBQUAMF4xCzAJBgNV
|
||||
BAYTAlVTMQ8wDQYDVQQIEwZOZXZhZGExEjAQBgNVBAcTCUxhcyBWZWdhczEaMBgG
|
||||
A1UEChMRZ2l0aHViLmNvbS9saWIvcHExDjAMBgNVBAMTBXBxIENBMB4XDTE0MTAx
|
||||
MTE1MDQyOVoXDTI0MTAwODE1MDQyOVowXjELMAkGA1UEBhMCVVMxDzANBgNVBAgT
|
||||
Bk5ldmFkYTESMBAGA1UEBxMJTGFzIFZlZ2FzMRowGAYDVQQKExFnaXRodWIuY29t
|
||||
L2xpYi9wcTEOMAwGA1UEAxMFcHEgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||
ggEKAoIBAQCV4PxP7ShzWBzUCThcKk3qZtOLtHmszQVtbqhvgTpm1kTRtKBdVMu0
|
||||
pLAHQ3JgJCnAYgH0iZxVGoMP16T3irdgsdC48+nNTFM2T0cCdkfDURGIhSFN47cb
|
||||
Pgy306BcDUD2q7ucW33+dlFSRuGVewocoh4BWM/vMtMvvWzdi4Ag/L/jhb+5wZxZ
|
||||
sWymsadOVSDePEMKOvlCa3EdVwVFV40TVyDb+iWBUivDAYsS2a3KajuJrO6MbZiE
|
||||
Sp2RCIkZS2zFmzWxVRi9ZhzIZhh7EVF9JAaNC3T52jhGUdlRq3YpBTMnd89iOh74
|
||||
6jWXG7wSuPj3haFzyNhmJ0ZUh+2Ynoh1AgMBAAGjgcMwgcAwHQYDVR0OBBYEFFKT
|
||||
7R52Cp9lT94ZZsHVIkA1y6ByMIGQBgNVHSMEgYgwgYWAFFKT7R52Cp9lT94ZZsHV
|
||||
IkA1y6ByoWKkYDBeMQswCQYDVQQGEwJVUzEPMA0GA1UECBMGTmV2YWRhMRIwEAYD
|
||||
VQQHEwlMYXMgVmVnYXMxGjAYBgNVBAoTEWdpdGh1Yi5jb20vbGliL3BxMQ4wDAYD
|
||||
VQQDEwVwcSBDQYIJANmheROCdW1NMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
|
||||
BQADggEBAAEhCLWkqJNMI8b4gkbmj5fqQ/4+oO83bZ3w2Oqf6eZ8I8BC4f2NOyE6
|
||||
tRUlq5+aU7eqC1cOAvGjO+YHN/bF/DFpwLlzvUSXt+JP/pYcUjL7v+pIvwqec9hD
|
||||
ndvM4iIbkD/H/OYQ3L+N3W+G1x7AcFIX+bGCb3PzYVQAjxreV6//wgKBosMGFbZo
|
||||
HPxT9RPMun61SViF04H5TNs0derVn1+5eiiYENeAhJzQNyZoOOUuX1X/Inx9bEPh
|
||||
C5vFBtSMgIytPgieRJVWAiMLYsfpIAStrHztRAbBs2DU01LmMgRvHdxgFEKinC/d
|
||||
UHZZQDP+6pT+zADrGhQGXe4eThaO6f0=
|
||||
-----END CERTIFICATE-----
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 1 (0x1)
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: C=US, ST=Nevada, L=Las Vegas, O=github.com/lib/pq, CN=pq CA
|
||||
Validity
|
||||
Not Before: Oct 11 15:05:15 2014 GMT
|
||||
Not After : Oct 8 15:05:15 2024 GMT
|
||||
Subject: C=US, ST=Nevada, L=Las Vegas, O=github.com/lib/pq, CN=postgres
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
RSA Public Key: (2048 bit)
|
||||
Modulus (2048 bit):
|
||||
00:d7:8a:4c:85:fb:17:a5:3c:8f:e0:72:11:29:ce:
|
||||
3f:b0:1f:3f:7d:c6:ee:7f:a7:fc:02:2b:35:47:08:
|
||||
a6:3d:90:df:5c:56:14:94:00:c7:6d:d1:d2:e2:61:
|
||||
95:77:b8:e3:a6:66:31:f9:1f:21:7d:62:e1:27:da:
|
||||
94:37:61:4a:ea:63:53:a0:61:b8:9c:bb:a5:e2:e7:
|
||||
b7:a6:d8:0f:05:04:c7:29:e2:ea:49:2b:7f:de:15:
|
||||
00:a6:18:70:50:c7:0c:de:9a:f9:5a:96:b0:e1:94:
|
||||
06:c6:6d:4a:21:3b:b4:0f:a5:6d:92:86:34:b2:4e:
|
||||
d7:0e:a7:19:c0:77:0b:7b:87:c8:92:de:42:ff:86:
|
||||
d2:b7:9a:a4:d4:15:23:ca:ad:a5:69:21:b8:ce:7e:
|
||||
66:cb:85:5d:b9:ed:8b:2d:09:8d:94:e4:04:1e:72:
|
||||
ec:ef:d0:76:90:15:5a:a4:f7:91:4b:e9:ce:4e:9d:
|
||||
5d:9a:70:17:9c:d8:e9:73:83:ea:3d:61:99:a6:cd:
|
||||
ac:91:40:5a:88:77:e5:4e:2a:8e:3d:13:f3:f9:38:
|
||||
6f:81:6b:8a:95:ca:0e:07:ab:6f:da:b4:8c:d9:ff:
|
||||
aa:78:03:aa:c7:c2:cf:6f:64:92:d3:d8:83:d5:af:
|
||||
f1:23:18:a7:2e:7b:17:0b:e7:7d:f1:fa:a8:41:a3:
|
||||
04:57
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Subject Key Identifier:
|
||||
EE:F0:B3:46:DC:C7:09:EB:0E:B6:2F:E5:FE:62:60:45:44:9F:59:CC
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:52:93:ED:1E:76:0A:9F:65:4F:DE:19:66:C1:D5:22:40:35:CB:A0:72
|
||||
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
X509v3 Key Usage:
|
||||
Digital Signature, Non Repudiation, Key Encipherment
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
7e:5a:6e:be:bf:d2:6c:c1:d6:fa:b6:fb:3f:06:53:36:08:87:
|
||||
9d:95:b1:39:af:9e:f6:47:38:17:39:da:25:7c:f2:ad:0c:e3:
|
||||
ab:74:19:ca:fb:8c:a0:50:c0:1d:19:8a:9c:21:ed:0f:3a:d1:
|
||||
96:54:2e:10:09:4f:b8:70:f7:2b:99:43:d2:c6:15:bc:3f:24:
|
||||
7d:28:39:32:3f:8d:a4:4f:40:75:7f:3e:0d:1c:d1:69:f2:4e:
|
||||
98:83:47:97:d2:25:ac:c9:36:86:2f:04:a6:c4:86:c7:c4:00:
|
||||
5f:7f:b9:ad:fc:bf:e9:f5:78:d7:82:1a:51:0d:fc:ab:9e:92:
|
||||
1d:5f:0c:18:d1:82:e0:14:c9:ce:91:89:71:ff:49:49:ff:35:
|
||||
bf:7b:44:78:42:c1:d0:66:65:bb:28:2e:60:ca:9b:20:12:a9:
|
||||
90:61:b1:96:ec:15:46:c9:37:f7:07:90:8a:89:45:2a:3f:37:
|
||||
ec:dc:e3:e5:8f:c3:3a:57:80:a5:54:60:0c:e1:b2:26:99:2b:
|
||||
40:7e:36:d1:9a:70:02:ec:63:f4:3b:72:ae:81:fb:30:20:6d:
|
||||
cb:48:46:c6:b5:8f:39:b1:84:05:25:55:8d:f5:62:f6:1b:46:
|
||||
2e:da:a3:4c:26:12:44:d7:56:b6:b8:a9:ca:d3:ab:71:45:7c:
|
||||
9f:48:6d:1e
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDlDCCAnygAwIBAgIBATANBgkqhkiG9w0BAQsFADBeMQswCQYDVQQGEwJVUzEP
|
||||
MA0GA1UECBMGTmV2YWRhMRIwEAYDVQQHEwlMYXMgVmVnYXMxGjAYBgNVBAoTEWdp
|
||||
dGh1Yi5jb20vbGliL3BxMQ4wDAYDVQQDEwVwcSBDQTAeFw0xNDEwMTExNTA1MTVa
|
||||
Fw0yNDEwMDgxNTA1MTVaMGExCzAJBgNVBAYTAlVTMQ8wDQYDVQQIEwZOZXZhZGEx
|
||||
EjAQBgNVBAcTCUxhcyBWZWdhczEaMBgGA1UEChMRZ2l0aHViLmNvbS9saWIvcHEx
|
||||
ETAPBgNVBAMTCHBvc3RncmVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
AQEA14pMhfsXpTyP4HIRKc4/sB8/fcbuf6f8Ais1RwimPZDfXFYUlADHbdHS4mGV
|
||||
d7jjpmYx+R8hfWLhJ9qUN2FK6mNToGG4nLul4ue3ptgPBQTHKeLqSSt/3hUAphhw
|
||||
UMcM3pr5Wpaw4ZQGxm1KITu0D6VtkoY0sk7XDqcZwHcLe4fIkt5C/4bSt5qk1BUj
|
||||
yq2laSG4zn5my4Vdue2LLQmNlOQEHnLs79B2kBVapPeRS+nOTp1dmnAXnNjpc4Pq
|
||||
PWGZps2skUBaiHflTiqOPRPz+ThvgWuKlcoOB6tv2rSM2f+qeAOqx8LPb2SS09iD
|
||||
1a/xIxinLnsXC+d98fqoQaMEVwIDAQABo1owWDAdBgNVHQ4EFgQU7vCzRtzHCesO
|
||||
ti/l/mJgRUSfWcwwHwYDVR0jBBgwFoAUUpPtHnYKn2VP3hlmwdUiQDXLoHIwCQYD
|
||||
VR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQELBQADggEBAH5abr6/0mzB
|
||||
1vq2+z8GUzYIh52VsTmvnvZHOBc52iV88q0M46t0Gcr7jKBQwB0Zipwh7Q860ZZU
|
||||
LhAJT7hw9yuZQ9LGFbw/JH0oOTI/jaRPQHV/Pg0c0WnyTpiDR5fSJazJNoYvBKbE
|
||||
hsfEAF9/ua38v+n1eNeCGlEN/Kuekh1fDBjRguAUyc6RiXH/SUn/Nb97RHhCwdBm
|
||||
ZbsoLmDKmyASqZBhsZbsFUbJN/cHkIqJRSo/N+zc4+WPwzpXgKVUYAzhsiaZK0B+
|
||||
NtGacALsY/Q7cq6B+zAgbctIRsa1jzmxhAUlVY31YvYbRi7ao0wmEkTXVra4qcrT
|
||||
q3FFfJ9IbR4=
|
||||
-----END CERTIFICATE-----
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEA14pMhfsXpTyP4HIRKc4/sB8/fcbuf6f8Ais1RwimPZDfXFYU
|
||||
lADHbdHS4mGVd7jjpmYx+R8hfWLhJ9qUN2FK6mNToGG4nLul4ue3ptgPBQTHKeLq
|
||||
SSt/3hUAphhwUMcM3pr5Wpaw4ZQGxm1KITu0D6VtkoY0sk7XDqcZwHcLe4fIkt5C
|
||||
/4bSt5qk1BUjyq2laSG4zn5my4Vdue2LLQmNlOQEHnLs79B2kBVapPeRS+nOTp1d
|
||||
mnAXnNjpc4PqPWGZps2skUBaiHflTiqOPRPz+ThvgWuKlcoOB6tv2rSM2f+qeAOq
|
||||
x8LPb2SS09iD1a/xIxinLnsXC+d98fqoQaMEVwIDAQABAoIBAF3ZoihUhJ82F4+r
|
||||
Gz4QyDpv4L1reT2sb1aiabhcU8ZK5nbWJG+tRyjSS/i2dNaEcttpdCj9HR/zhgZM
|
||||
bm0OuAgG58rVwgS80CZUruq++Qs+YVojq8/gWPTiQD4SNhV2Fmx3HkwLgUk3oxuT
|
||||
SsvdqzGE3okGVrutCIcgy126eA147VPMoej1Bb3fO6npqK0pFPhZfAc0YoqJuM+k
|
||||
obRm5pAnGUipyLCFXjA9HYPKwYZw2RtfdA3CiImHeanSdqS+ctrC9y8BV40Th7gZ
|
||||
haXdKUNdjmIxV695QQ1mkGqpKLZFqhzKioGQ2/Ly2d1iaKN9fZltTusu8unepWJ2
|
||||
tlT9qMECgYEA9uHaF1t2CqE+AJvWTihHhPIIuLxoOQXYea1qvxfcH/UMtaLKzCNm
|
||||
lQ5pqCGsPvp+10f36yttO1ZehIvlVNXuJsjt0zJmPtIolNuJY76yeussfQ9jHheB
|
||||
5uPEzCFlHzxYbBUyqgWaF6W74okRGzEGJXjYSP0yHPPdU4ep2q3bGiUCgYEA34Af
|
||||
wBSuQSK7uLxArWHvQhyuvi43ZGXls6oRGl+Ysj54s8BP6XGkq9hEJ6G4yxgyV+BR
|
||||
DUOs5X8/TLT8POuIMYvKTQthQyCk0eLv2FLdESDuuKx0kBVY3s8lK3/z5HhrdOiN
|
||||
VMNZU+xDKgKc3hN9ypkk8vcZe6EtH7Y14e0rVcsCgYBTgxi8F/M5K0wG9rAqphNz
|
||||
VFBA9XKn/2M33cKjO5X5tXIEKzpAjaUQvNxexG04rJGljzG8+mar0M6ONahw5yD1
|
||||
O7i/XWgazgpuOEkkVYiYbd8RutfDgR4vFVMn3hAP3eDnRtBplRWH9Ec3HTiNIys6
|
||||
F8PKBOQjyRZQQC7jyzW3hQKBgACe5HeuFwXLSOYsb6mLmhR+6+VPT4wR1F95W27N
|
||||
USk9jyxAnngxfpmTkiziABdgS9N+pfr5cyN4BP77ia/Jn6kzkC5Cl9SN5KdIkA3z
|
||||
vPVtN/x/ThuQU5zaymmig1ThGLtMYggYOslG4LDfLPxY5YKIhle+Y+259twdr2yf
|
||||
Mf2dAoGAaGv3tWMgnIdGRk6EQL/yb9PKHo7ShN+tKNlGaK7WwzBdKs+Fe8jkgcr7
|
||||
pz4Ne887CmxejdISzOCcdT+Zm9Bx6I/uZwWOtDvWpIgIxVX9a9URj/+D1MxTE/y4
|
||||
d6H+c89yDY62I2+drMpdjCd3EtCaTlxpTbRS+s1eAHMH7aEkcCE=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
-1244
File diff suppressed because it is too large
Load Diff
-61
@@ -1,61 +0,0 @@
|
||||
// +build go1.1
|
||||
|
||||
package pq
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestXactMultiStmt(t *testing.T) {
|
||||
// minified test case based on bug reports from
|
||||
// pico303@gmail.com and rangelspam@gmail.com
|
||||
t.Skip("Skipping failing test")
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tx.Commit()
|
||||
|
||||
rows, err := tx.Query("select 1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if rows.Next() {
|
||||
var val int32
|
||||
if err = rows.Scan(&val); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Expected at least one row in first query in xact")
|
||||
}
|
||||
|
||||
rows2, err := tx.Query("select 2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if rows2.Next() {
|
||||
var val2 int32
|
||||
if err := rows2.Scan(&val2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Expected at least one row in second query in xact")
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = rows2.Err(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
-324
@@ -1,324 +0,0 @@
|
||||
package pq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCopyInStmt(t *testing.T) {
|
||||
var stmt string
|
||||
stmt = CopyIn("table name")
|
||||
if stmt != `COPY "table name" () FROM STDIN` {
|
||||
t.Fatal(stmt)
|
||||
}
|
||||
|
||||
stmt = CopyIn("table name", "column 1", "column 2")
|
||||
if stmt != `COPY "table name" ("column 1", "column 2") FROM STDIN` {
|
||||
t.Fatal(stmt)
|
||||
}
|
||||
|
||||
stmt = CopyIn(`table " name """`, `co"lumn""`)
|
||||
if stmt != `COPY "table "" name """"""" ("co""lumn""""") FROM STDIN` {
|
||||
t.Fatal(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyInSchemaStmt(t *testing.T) {
|
||||
var stmt string
|
||||
stmt = CopyInSchema("schema name", "table name")
|
||||
if stmt != `COPY "schema name"."table name" () FROM STDIN` {
|
||||
t.Fatal(stmt)
|
||||
}
|
||||
|
||||
stmt = CopyInSchema("schema name", "table name", "column 1", "column 2")
|
||||
if stmt != `COPY "schema name"."table name" ("column 1", "column 2") FROM STDIN` {
|
||||
t.Fatal(stmt)
|
||||
}
|
||||
|
||||
stmt = CopyInSchema(`schema " name """`, `table " name """`, `co"lumn""`)
|
||||
if stmt != `COPY "schema "" name """"""".`+
|
||||
`"table "" name """"""" ("co""lumn""""") FROM STDIN` {
|
||||
t.Fatal(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyInMultipleValues(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
txn, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer txn.Rollback()
|
||||
|
||||
_, err = txn.Exec("CREATE TEMP TABLE temp (a int, b varchar)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, err := txn.Prepare(CopyIn("temp", "a", "b"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
longString := strings.Repeat("#", 500)
|
||||
|
||||
for i := 0; i < 500; i++ {
|
||||
_, err = stmt.Exec(int64(i), longString)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = stmt.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var num int
|
||||
err = txn.QueryRow("SELECT COUNT(*) FROM temp").Scan(&num)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if num != 500 {
|
||||
t.Fatalf("expected 500 items, not %d", num)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyInTypes(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
txn, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer txn.Rollback()
|
||||
|
||||
_, err = txn.Exec("CREATE TEMP TABLE temp (num INTEGER, text VARCHAR, blob BYTEA, nothing VARCHAR)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, err := txn.Prepare(CopyIn("temp", "num", "text", "blob", "nothing"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(int64(1234567890), "Héllö\n ☃!\r\t\\", []byte{0, 255, 9, 10, 13}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = stmt.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var num int
|
||||
var text string
|
||||
var blob []byte
|
||||
var nothing sql.NullString
|
||||
|
||||
err = txn.QueryRow("SELECT * FROM temp").Scan(&num, &text, &blob, ¬hing)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if num != 1234567890 {
|
||||
t.Fatal("unexpected result", num)
|
||||
}
|
||||
if text != "Héllö\n ☃!\r\t\\" {
|
||||
t.Fatal("unexpected result", text)
|
||||
}
|
||||
if bytes.Compare(blob, []byte{0, 255, 9, 10, 13}) != 0 {
|
||||
t.Fatal("unexpected result", blob)
|
||||
}
|
||||
if nothing.Valid {
|
||||
t.Fatal("unexpected result", nothing.String)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyInWrongType(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
txn, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer txn.Rollback()
|
||||
|
||||
_, err = txn.Exec("CREATE TEMP TABLE temp (num INTEGER)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, err := txn.Prepare(CopyIn("temp", "num"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec("Héllö\n ☃!\r\t\\")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = stmt.Exec()
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if pge := err.(*Error); pge.Code.Name() != "invalid_text_representation" {
|
||||
t.Fatalf("expected 'invalid input syntax for integer' error, got %s (%+v)", pge.Code.Name(), pge)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyOutsideOfTxnError(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
_, err := db.Prepare(CopyIn("temp", "num"))
|
||||
if err == nil {
|
||||
t.Fatal("COPY outside of transaction did not return an error")
|
||||
}
|
||||
if err != errCopyNotSupportedOutsideTxn {
|
||||
t.Fatalf("expected %s, got %s", err, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyInBinaryError(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
txn, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer txn.Rollback()
|
||||
|
||||
_, err = txn.Exec("CREATE TEMP TABLE temp (num INTEGER)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = txn.Prepare("COPY temp (num) FROM STDIN WITH binary")
|
||||
if err != errBinaryCopyNotSupported {
|
||||
t.Fatalf("expected %s, got %+v", errBinaryCopyNotSupported, err)
|
||||
}
|
||||
// check that the protocol is in a valid state
|
||||
err = txn.Rollback()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyFromError(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
txn, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer txn.Rollback()
|
||||
|
||||
_, err = txn.Exec("CREATE TEMP TABLE temp (num INTEGER)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = txn.Prepare("COPY temp (num) TO STDOUT")
|
||||
if err != errCopyToNotSupported {
|
||||
t.Fatalf("expected %s, got %+v", errCopyToNotSupported, err)
|
||||
}
|
||||
// check that the protocol is in a valid state
|
||||
err = txn.Rollback()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopySyntaxError(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
txn, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer txn.Rollback()
|
||||
|
||||
_, err = txn.Prepare("COPY ")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if pge := err.(*Error); pge.Code.Name() != "syntax_error" {
|
||||
t.Fatalf("expected syntax error, got %s (%+v)", pge.Code.Name(), pge)
|
||||
}
|
||||
// check that the protocol is in a valid state
|
||||
err = txn.Rollback()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCopyIn(b *testing.B) {
|
||||
db := openTestConn(b)
|
||||
defer db.Close()
|
||||
|
||||
txn, err := db.Begin()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer txn.Rollback()
|
||||
|
||||
_, err = txn.Exec("CREATE TEMP TABLE temp (a int, b varchar)")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, err := txn.Prepare(CopyIn("temp", "a", "b"))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err = stmt.Exec(int64(i), "hello world!")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = stmt.Exec()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var num int
|
||||
err = txn.QueryRow("SELECT COUNT(*) FROM temp").Scan(&num)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
if num != b.N {
|
||||
b.Fatalf("expected %d items, not %d", b.N, num)
|
||||
}
|
||||
}
|
||||
-427
@@ -1,427 +0,0 @@
|
||||
package pq
|
||||
|
||||
import (
|
||||
"github.com/lib/pq/oid"
|
||||
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestScanTimestamp(t *testing.T) {
|
||||
var nt NullTime
|
||||
tn := time.Now()
|
||||
nt.Scan(tn)
|
||||
if !nt.Valid {
|
||||
t.Errorf("Expected Valid=false")
|
||||
}
|
||||
if nt.Time != tn {
|
||||
t.Errorf("Time value mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanNilTimestamp(t *testing.T) {
|
||||
var nt NullTime
|
||||
nt.Scan(nil)
|
||||
if nt.Valid {
|
||||
t.Errorf("Expected Valid=false")
|
||||
}
|
||||
}
|
||||
|
||||
var timeTests = []struct {
|
||||
str string
|
||||
timeval time.Time
|
||||
}{
|
||||
{"22001-02-03", time.Date(22001, time.February, 3, 0, 0, 0, 0, time.FixedZone("", 0))},
|
||||
{"2001-02-03", time.Date(2001, time.February, 3, 0, 0, 0, 0, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06", time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.000001", time.Date(2001, time.February, 3, 4, 5, 6, 1000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.00001", time.Date(2001, time.February, 3, 4, 5, 6, 10000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.0001", time.Date(2001, time.February, 3, 4, 5, 6, 100000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.001", time.Date(2001, time.February, 3, 4, 5, 6, 1000000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.01", time.Date(2001, time.February, 3, 4, 5, 6, 10000000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.1", time.Date(2001, time.February, 3, 4, 5, 6, 100000000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.12", time.Date(2001, time.February, 3, 4, 5, 6, 120000000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.123", time.Date(2001, time.February, 3, 4, 5, 6, 123000000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.1234", time.Date(2001, time.February, 3, 4, 5, 6, 123400000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.12345", time.Date(2001, time.February, 3, 4, 5, 6, 123450000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.123456", time.Date(2001, time.February, 3, 4, 5, 6, 123456000, time.FixedZone("", 0))},
|
||||
{"2001-02-03 04:05:06.123-07", time.Date(2001, time.February, 3, 4, 5, 6, 123000000,
|
||||
time.FixedZone("", -7*60*60))},
|
||||
{"2001-02-03 04:05:06-07", time.Date(2001, time.February, 3, 4, 5, 6, 0,
|
||||
time.FixedZone("", -7*60*60))},
|
||||
{"2001-02-03 04:05:06-07:42", time.Date(2001, time.February, 3, 4, 5, 6, 0,
|
||||
time.FixedZone("", -(7*60*60+42*60)))},
|
||||
{"2001-02-03 04:05:06-07:30:09", time.Date(2001, time.February, 3, 4, 5, 6, 0,
|
||||
time.FixedZone("", -(7*60*60+30*60+9)))},
|
||||
{"2001-02-03 04:05:06+07", time.Date(2001, time.February, 3, 4, 5, 6, 0,
|
||||
time.FixedZone("", 7*60*60))},
|
||||
//{"10000-02-03 04:05:06 BC", time.Date(-10000, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0))},
|
||||
{"0010-02-03 04:05:06 BC", time.Date(-10, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0))},
|
||||
{"0010-02-03 04:05:06.123 BC", time.Date(-10, time.February, 3, 4, 5, 6, 123000000, time.FixedZone("", 0))},
|
||||
{"0010-02-03 04:05:06.123-07 BC", time.Date(-10, time.February, 3, 4, 5, 6, 123000000,
|
||||
time.FixedZone("", -7*60*60))},
|
||||
}
|
||||
|
||||
// Helper function for the two tests below
|
||||
func tryParse(str string) (t time.Time, err error) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
err = fmt.Errorf("%v", p)
|
||||
return
|
||||
}
|
||||
}()
|
||||
t = parseTs(nil, str)
|
||||
return
|
||||
}
|
||||
|
||||
// Test that parsing the string results in the expected value.
|
||||
func TestParseTs(t *testing.T) {
|
||||
for i, tt := range timeTests {
|
||||
val, err := tryParse(tt.str)
|
||||
if err != nil {
|
||||
t.Errorf("%d: got error: %v", i, err)
|
||||
} else if val.String() != tt.timeval.String() {
|
||||
t.Errorf("%d: expected to parse %q into %q; got %q",
|
||||
i, tt.str, tt.timeval, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now test that sending the value into the database and parsing it back
|
||||
// returns the same time.Time value.
|
||||
func TestEncodeAndParseTs(t *testing.T) {
|
||||
db, err := openTestConnConninfo("timezone='Etc/UTC'")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
for i, tt := range timeTests {
|
||||
var dbstr string
|
||||
err = db.QueryRow("SELECT ($1::timestamptz)::text", tt.timeval).Scan(&dbstr)
|
||||
if err != nil {
|
||||
t.Errorf("%d: could not send value %q to the database: %s", i, tt.timeval, err)
|
||||
continue
|
||||
}
|
||||
|
||||
val, err := tryParse(dbstr)
|
||||
if err != nil {
|
||||
t.Errorf("%d: could not parse value %q: %s", i, dbstr, err)
|
||||
continue
|
||||
}
|
||||
val = val.In(tt.timeval.Location())
|
||||
if val.String() != tt.timeval.String() {
|
||||
t.Errorf("%d: expected to parse %q into %q; got %q", i, dbstr, tt.timeval, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var formatTimeTests = []struct {
|
||||
time time.Time
|
||||
expected string
|
||||
}{
|
||||
{time.Time{}, "0001-01-01T00:00:00Z"},
|
||||
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "2001-02-03T04:05:06.123456789Z"},
|
||||
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "2001-02-03T04:05:06.123456789+02:00"},
|
||||
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "2001-02-03T04:05:06.123456789-06:00"},
|
||||
{time.Date(1, time.January, 1, 0, 0, 0, 0, time.FixedZone("", 19*60+32)), "0001-01-01T00:00:00+00:19:32"},
|
||||
{time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "2001-02-03T04:05:06-07:30:09"},
|
||||
}
|
||||
|
||||
func TestFormatTs(t *testing.T) {
|
||||
for i, tt := range formatTimeTests {
|
||||
val := string(formatTs(tt.time))
|
||||
if val != tt.expected {
|
||||
t.Errorf("%d: incorrect time format %q, want %q", i, val, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestampWithTimeZone(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// try several different locations, all included in Go's zoneinfo.zip
|
||||
for _, locName := range []string{
|
||||
"UTC",
|
||||
"America/Chicago",
|
||||
"America/New_York",
|
||||
"Australia/Darwin",
|
||||
"Australia/Perth",
|
||||
} {
|
||||
loc, err := time.LoadLocation(locName)
|
||||
if err != nil {
|
||||
t.Logf("Could not load time zone %s - skipping", locName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Postgres timestamps have a resolution of 1 microsecond, so don't
|
||||
// use the full range of the Nanosecond argument
|
||||
refTime := time.Date(2012, 11, 6, 10, 23, 42, 123456000, loc)
|
||||
|
||||
for _, pgTimeZone := range []string{"US/Eastern", "Australia/Darwin"} {
|
||||
// Switch Postgres's timezone to test different output timestamp formats
|
||||
_, err = tx.Exec(fmt.Sprintf("set time zone '%s'", pgTimeZone))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var gotTime time.Time
|
||||
row := tx.QueryRow("select $1::timestamp with time zone", refTime)
|
||||
err = row.Scan(&gotTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !refTime.Equal(gotTime) {
|
||||
t.Errorf("timestamps not equal: %s != %s", refTime, gotTime)
|
||||
}
|
||||
|
||||
// check that the time zone is set correctly based on TimeZone
|
||||
pgLoc, err := time.LoadLocation(pgTimeZone)
|
||||
if err != nil {
|
||||
t.Logf("Could not load time zone %s - skipping", pgLoc)
|
||||
continue
|
||||
}
|
||||
translated := refTime.In(pgLoc)
|
||||
if translated.String() != gotTime.String() {
|
||||
t.Errorf("timestamps not equal: %s != %s", translated, gotTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestampWithOutTimezone(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
test := func(ts, pgts string) {
|
||||
r, err := db.Query("SELECT $1::timestamp", pgts)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not run query: %v", err)
|
||||
}
|
||||
|
||||
n := r.Next()
|
||||
|
||||
if n != true {
|
||||
t.Fatal("Expected at least one row")
|
||||
}
|
||||
|
||||
var result time.Time
|
||||
err = r.Scan(&result)
|
||||
if err != nil {
|
||||
t.Fatalf("Did not expect error scanning row: %v", err)
|
||||
}
|
||||
|
||||
expected, err := time.Parse(time.RFC3339, ts)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse test time literal: %v", err)
|
||||
}
|
||||
|
||||
if !result.Equal(expected) {
|
||||
t.Fatalf("Expected time to match %v: got mismatch %v",
|
||||
expected, result)
|
||||
}
|
||||
|
||||
n = r.Next()
|
||||
if n != false {
|
||||
t.Fatal("Expected only one row")
|
||||
}
|
||||
}
|
||||
|
||||
test("2000-01-01T00:00:00Z", "2000-01-01T00:00:00")
|
||||
|
||||
// Test higher precision time
|
||||
test("2013-01-04T20:14:58.80033Z", "2013-01-04 20:14:58.80033")
|
||||
}
|
||||
|
||||
func TestStringWithNul(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
hello0world := string("hello\x00world")
|
||||
_, err := db.Query("SELECT $1::text", &hello0world)
|
||||
if err == nil {
|
||||
t.Fatal("Postgres accepts a string with nul in it; " +
|
||||
"injection attacks may be plausible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteaToText(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
b := []byte("hello world")
|
||||
row := db.QueryRow("SELECT $1::text", b)
|
||||
|
||||
var result []byte
|
||||
err := row.Scan(&result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(result) != string(b) {
|
||||
t.Fatalf("expected %v but got %v", b, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextToBytea(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
b := "hello world"
|
||||
row := db.QueryRow("SELECT $1::bytea", b)
|
||||
|
||||
var result []byte
|
||||
err := row.Scan(&result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(result, []byte(b)) {
|
||||
t.Fatalf("expected %v but got %v", b, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteaOutputFormatEncoding(t *testing.T) {
|
||||
input := []byte("\\x\x00\x01\x02\xFF\xFEabcdefg0123")
|
||||
want := []byte("\\x5c78000102fffe6162636465666730313233")
|
||||
got := encode(¶meterStatus{serverVersion: 90000}, input, oid.T_bytea)
|
||||
if !bytes.Equal(want, got) {
|
||||
t.Errorf("invalid hex bytea output, got %v but expected %v", got, want)
|
||||
}
|
||||
|
||||
want = []byte("\\\\x\\000\\001\\002\\377\\376abcdefg0123")
|
||||
got = encode(¶meterStatus{serverVersion: 84000}, input, oid.T_bytea)
|
||||
if !bytes.Equal(want, got) {
|
||||
t.Errorf("invalid escape bytea output, got %v but expected %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteaOutputFormats(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
if getServerVersion(t, db) < 90000 {
|
||||
// skip
|
||||
return
|
||||
}
|
||||
|
||||
testByteaOutputFormat := func(f string) {
|
||||
expectedData := []byte("\x5c\x78\x00\xff\x61\x62\x63\x01\x08")
|
||||
sqlQuery := "SELECT decode('5c7800ff6162630108', 'hex')"
|
||||
|
||||
var data []byte
|
||||
|
||||
// use a txn to avoid relying on getting the same connection
|
||||
txn, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer txn.Rollback()
|
||||
|
||||
_, err = txn.Exec("SET LOCAL bytea_output TO " + f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// use Query; QueryRow would hide the actual error
|
||||
rows, err := txn.Query(sqlQuery)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !rows.Next() {
|
||||
if rows.Err() != nil {
|
||||
t.Fatal(rows.Err())
|
||||
}
|
||||
t.Fatal("shouldn't happen")
|
||||
}
|
||||
err = rows.Scan(&data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = rows.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(data, expectedData) {
|
||||
t.Errorf("unexpected bytea value %v for format %s; expected %v", data, f, expectedData)
|
||||
}
|
||||
}
|
||||
|
||||
testByteaOutputFormat("hex")
|
||||
testByteaOutputFormat("escape")
|
||||
}
|
||||
|
||||
func TestAppendEncodedText(t *testing.T) {
|
||||
var buf []byte
|
||||
|
||||
buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, int64(10))
|
||||
buf = append(buf, '\t')
|
||||
buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, float32(42.0000000001))
|
||||
buf = append(buf, '\t')
|
||||
buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, 42.0000000001)
|
||||
buf = append(buf, '\t')
|
||||
buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, "hello\tworld")
|
||||
buf = append(buf, '\t')
|
||||
buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, []byte{0, 128, 255})
|
||||
|
||||
if string(buf) != "10\t42\t42.0000000001\thello\\tworld\t\\\\x0080ff" {
|
||||
t.Fatal(string(buf))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendEscapedText(t *testing.T) {
|
||||
if esc := appendEscapedText(nil, "hallo\tescape"); string(esc) != "hallo\\tescape" {
|
||||
t.Fatal(string(esc))
|
||||
}
|
||||
if esc := appendEscapedText(nil, "hallo\\tescape\n"); string(esc) != "hallo\\\\tescape\\n" {
|
||||
t.Fatal(string(esc))
|
||||
}
|
||||
if esc := appendEscapedText(nil, "\n\r\t\f"); string(esc) != "\\n\\r\\t\f" {
|
||||
t.Fatal(string(esc))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendEscapedTextExistingBuffer(t *testing.T) {
|
||||
var buf []byte
|
||||
buf = []byte("123\t")
|
||||
if esc := appendEscapedText(buf, "hallo\tescape"); string(esc) != "123\thallo\\tescape" {
|
||||
t.Fatal(string(esc))
|
||||
}
|
||||
buf = []byte("123\t")
|
||||
if esc := appendEscapedText(buf, "hallo\\tescape\n"); string(esc) != "123\thallo\\\\tescape\\n" {
|
||||
t.Fatal(string(esc))
|
||||
}
|
||||
buf = []byte("123\t")
|
||||
if esc := appendEscapedText(buf, "\n\r\t\f"); string(esc) != "123\t\\n\\r\\t\f" {
|
||||
t.Fatal(string(esc))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendEscapedText(b *testing.B) {
|
||||
longString := ""
|
||||
for i := 0; i < 100; i++ {
|
||||
longString += "123456789\n"
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
appendEscapedText(nil, longString)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendEscapedTextNoEscape(b *testing.B) {
|
||||
longString := ""
|
||||
for i := 0; i < 100; i++ {
|
||||
longString += "1234567890"
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
appendEscapedText(nil, longString)
|
||||
}
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
package hstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A wrapper for transferring Hstore values back and forth easily.
|
||||
type Hstore struct {
|
||||
Map map[string]sql.NullString
|
||||
}
|
||||
|
||||
// escapes and quotes hstore keys/values
|
||||
// s should be a sql.NullString or string
|
||||
func hQuote(s interface{}) string {
|
||||
var str string
|
||||
switch v := s.(type) {
|
||||
case sql.NullString:
|
||||
if !v.Valid {
|
||||
return "NULL"
|
||||
}
|
||||
str = v.String
|
||||
case string:
|
||||
str = v
|
||||
default:
|
||||
panic("not a string or sql.NullString")
|
||||
}
|
||||
|
||||
str = strings.Replace(str, "\\", "\\\\", -1)
|
||||
return `"` + strings.Replace(str, "\"", "\\\"", -1) + `"`
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
//
|
||||
// Note h.Map is reallocated before the scan to clear existing values. If the
|
||||
// hstore column's database value is NULL, then h.Map is set to nil instead.
|
||||
func (h *Hstore) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
h.Map = nil
|
||||
return nil
|
||||
}
|
||||
h.Map = make(map[string]sql.NullString)
|
||||
var b byte
|
||||
pair := [][]byte{{}, {}}
|
||||
pi := 0
|
||||
inQuote := false
|
||||
didQuote := false
|
||||
sawSlash := false
|
||||
bindex := 0
|
||||
for bindex, b = range value.([]byte) {
|
||||
if sawSlash {
|
||||
pair[pi] = append(pair[pi], b)
|
||||
sawSlash = false
|
||||
continue
|
||||
}
|
||||
|
||||
switch b {
|
||||
case '\\':
|
||||
sawSlash = true
|
||||
continue
|
||||
case '"':
|
||||
inQuote = !inQuote
|
||||
if !didQuote {
|
||||
didQuote = true
|
||||
}
|
||||
continue
|
||||
default:
|
||||
if !inQuote {
|
||||
switch b {
|
||||
case ' ', '\t', '\n', '\r':
|
||||
continue
|
||||
case '=':
|
||||
continue
|
||||
case '>':
|
||||
pi = 1
|
||||
didQuote = false
|
||||
continue
|
||||
case ',':
|
||||
s := string(pair[1])
|
||||
if !didQuote && len(s) == 4 && strings.ToLower(s) == "null" {
|
||||
h.Map[string(pair[0])] = sql.NullString{String: "", Valid: false}
|
||||
} else {
|
||||
h.Map[string(pair[0])] = sql.NullString{String: string(pair[1]), Valid: true}
|
||||
}
|
||||
pair[0] = []byte{}
|
||||
pair[1] = []byte{}
|
||||
pi = 0
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
pair[pi] = append(pair[pi], b)
|
||||
}
|
||||
if bindex > 0 {
|
||||
s := string(pair[1])
|
||||
if !didQuote && len(s) == 4 && strings.ToLower(s) == "null" {
|
||||
h.Map[string(pair[0])] = sql.NullString{String: "", Valid: false}
|
||||
} else {
|
||||
h.Map[string(pair[0])] = sql.NullString{String: string(pair[1]), Valid: true}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface. Note if h.Map is nil, the
|
||||
// database column value will be set to NULL.
|
||||
func (h Hstore) Value() (driver.Value, error) {
|
||||
if h.Map == nil {
|
||||
return nil, nil
|
||||
}
|
||||
parts := []string{}
|
||||
for key, val := range h.Map {
|
||||
thispart := hQuote(key) + "=>" + hQuote(val)
|
||||
parts = append(parts, thispart)
|
||||
}
|
||||
return []byte(strings.Join(parts, ",")), nil
|
||||
}
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
package hstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_ "github.com/lib/pq"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Fatalistic interface {
|
||||
Fatal(args ...interface{})
|
||||
}
|
||||
|
||||
func openTestConn(t Fatalistic) *sql.DB {
|
||||
datname := os.Getenv("PGDATABASE")
|
||||
sslmode := os.Getenv("PGSSLMODE")
|
||||
|
||||
if datname == "" {
|
||||
os.Setenv("PGDATABASE", "pqgotest")
|
||||
}
|
||||
|
||||
if sslmode == "" {
|
||||
os.Setenv("PGSSLMODE", "disable")
|
||||
}
|
||||
|
||||
conn, err := sql.Open("postgres", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func TestHstore(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
// quitely create hstore if it doesn't exist
|
||||
_, err := db.Exec("CREATE EXTENSION IF NOT EXISTS hstore")
|
||||
if err != nil {
|
||||
t.Log("Skipping hstore tests - hstore extension create failed. " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
hs := Hstore{}
|
||||
|
||||
// test for null-valued hstores
|
||||
err = db.QueryRow("SELECT NULL::hstore").Scan(&hs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hs.Map != nil {
|
||||
t.Fatalf("expected null map")
|
||||
}
|
||||
|
||||
err = db.QueryRow("SELECT $1::hstore", hs).Scan(&hs)
|
||||
if err != nil {
|
||||
t.Fatalf("re-query null map failed: %s", err.Error())
|
||||
}
|
||||
if hs.Map != nil {
|
||||
t.Fatalf("expected null map")
|
||||
}
|
||||
|
||||
// test for empty hstores
|
||||
err = db.QueryRow("SELECT ''::hstore").Scan(&hs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hs.Map == nil {
|
||||
t.Fatalf("expected empty map, got null map")
|
||||
}
|
||||
if len(hs.Map) != 0 {
|
||||
t.Fatalf("expected empty map, got len(map)=%d", len(hs.Map))
|
||||
}
|
||||
|
||||
err = db.QueryRow("SELECT $1::hstore", hs).Scan(&hs)
|
||||
if err != nil {
|
||||
t.Fatalf("re-query empty map failed: %s", err.Error())
|
||||
}
|
||||
if hs.Map == nil {
|
||||
t.Fatalf("expected empty map, got null map")
|
||||
}
|
||||
if len(hs.Map) != 0 {
|
||||
t.Fatalf("expected empty map, got len(map)=%d", len(hs.Map))
|
||||
}
|
||||
|
||||
// a few example maps to test out
|
||||
hsOnePair := Hstore{
|
||||
Map: map[string]sql.NullString{
|
||||
"key1": {"value1", true},
|
||||
},
|
||||
}
|
||||
|
||||
hsThreePairs := Hstore{
|
||||
Map: map[string]sql.NullString{
|
||||
"key1": {"value1", true},
|
||||
"key2": {"value2", true},
|
||||
"key3": {"value3", true},
|
||||
},
|
||||
}
|
||||
|
||||
hsSmorgasbord := Hstore{
|
||||
Map: map[string]sql.NullString{
|
||||
"nullstring": {"NULL", true},
|
||||
"actuallynull": {"", false},
|
||||
"NULL": {"NULL string key", true},
|
||||
"withbracket": {"value>42", true},
|
||||
"withequal": {"value=42", true},
|
||||
`"withquotes1"`: {`this "should" be fine`, true},
|
||||
`"withquotes"2"`: {`this "should\" also be fine`, true},
|
||||
"embedded1": {"value1=>x1", true},
|
||||
"embedded2": {`"value2"=>x2`, true},
|
||||
"withnewlines": {"\n\nvalue\t=>2", true},
|
||||
"<<all sorts of crazy>>": {`this, "should,\" also, => be fine`, true},
|
||||
},
|
||||
}
|
||||
|
||||
// test encoding in query params, then decoding during Scan
|
||||
testBidirectional := func(h Hstore) {
|
||||
err = db.QueryRow("SELECT $1::hstore", h).Scan(&hs)
|
||||
if err != nil {
|
||||
t.Fatalf("re-query %d-pair map failed: %s", len(h.Map), err.Error())
|
||||
}
|
||||
if hs.Map == nil {
|
||||
t.Fatalf("expected %d-pair map, got null map", len(h.Map))
|
||||
}
|
||||
if len(hs.Map) != len(h.Map) {
|
||||
t.Fatalf("expected %d-pair map, got len(map)=%d", len(h.Map), len(hs.Map))
|
||||
}
|
||||
|
||||
for key, val := range hs.Map {
|
||||
otherval, found := h.Map[key]
|
||||
if !found {
|
||||
t.Fatalf(" key '%v' not found in %d-pair map", key, len(h.Map))
|
||||
}
|
||||
if otherval.Valid != val.Valid {
|
||||
t.Fatalf(" value %v <> %v in %d-pair map", otherval, val, len(h.Map))
|
||||
}
|
||||
if otherval.String != val.String {
|
||||
t.Fatalf(" value '%v' <> '%v' in %d-pair map", otherval.String, val.String, len(h.Map))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testBidirectional(hsOnePair)
|
||||
testBidirectional(hsThreePairs)
|
||||
testBidirectional(hsSmorgasbord)
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
|
||||
Below you will find a self-contained Go program which uses the LISTEN / NOTIFY
|
||||
mechanism to avoid polling the database while waiting for more work to arrive.
|
||||
|
||||
//
|
||||
// You can see the program in action by defining a function similar to
|
||||
// the following:
|
||||
//
|
||||
// CREATE OR REPLACE FUNCTION public.get_work()
|
||||
// RETURNS bigint
|
||||
// LANGUAGE sql
|
||||
// AS $$
|
||||
// SELECT CASE WHEN random() >= 0.2 THEN int8 '1' END
|
||||
// $$
|
||||
// ;
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/lib/pq"
|
||||
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func doWork(db *sql.DB, work int64) {
|
||||
// work here
|
||||
}
|
||||
|
||||
func getWork(db *sql.DB) {
|
||||
for {
|
||||
// get work from the database here
|
||||
var work sql.NullInt64
|
||||
err := db.QueryRow("SELECT get_work()").Scan(&work)
|
||||
if err != nil {
|
||||
fmt.Println("call to get_work() failed: ", err)
|
||||
time.Sleep(10 * time.Second)
|
||||
continue
|
||||
}
|
||||
if !work.Valid {
|
||||
// no more work to do
|
||||
fmt.Println("ran out of work")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("starting work on ", work.Int64)
|
||||
go doWork(db, work.Int64)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForNotification(l *pq.Listener) {
|
||||
for {
|
||||
select {
|
||||
case <-l.Notify:
|
||||
fmt.Println("received notification, new work available")
|
||||
return
|
||||
case <-time.After(90 * time.Second):
|
||||
go func() {
|
||||
l.Ping()
|
||||
}()
|
||||
// Check if there's more work available, just in case it takes
|
||||
// a while for the Listener to notice connection loss and
|
||||
// reconnect.
|
||||
fmt.Println("received no work for 90 seconds, checking for new work")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
var conninfo string = ""
|
||||
|
||||
db, err := sql.Open("postgres", conninfo)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
reportProblem := func(ev pq.ListenerEventType, err error) {
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
listener := pq.NewListener(conninfo, 10 * time.Second, time.Minute, reportProblem)
|
||||
err = listener.Listen("getwork")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("entering main loop")
|
||||
for {
|
||||
// process all available work before waiting for notifications
|
||||
getWork(db)
|
||||
waitForNotification(listener)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
package listen_example
|
||||
-507
@@ -1,507 +0,0 @@
|
||||
package pq
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errNilNotification = errors.New("nil notification")
|
||||
|
||||
func expectNotification(t *testing.T, ch <-chan *Notification, relname string, extra string) error {
|
||||
select {
|
||||
case n := <-ch:
|
||||
if n == nil {
|
||||
return errNilNotification
|
||||
}
|
||||
if n.Channel != relname || n.Extra != extra {
|
||||
return fmt.Errorf("unexpected notification %v", n)
|
||||
}
|
||||
return nil
|
||||
case <-time.After(1500 * time.Millisecond):
|
||||
return fmt.Errorf("timeout")
|
||||
}
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func expectNoNotification(t *testing.T, ch <-chan *Notification) error {
|
||||
select {
|
||||
case n := <-ch:
|
||||
return fmt.Errorf("unexpected notification %v", n)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return nil
|
||||
}
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func expectEvent(t *testing.T, eventch <-chan ListenerEventType, et ListenerEventType) error {
|
||||
select {
|
||||
case e := <-eventch:
|
||||
if e != et {
|
||||
return fmt.Errorf("unexpected event %v", e)
|
||||
}
|
||||
return nil
|
||||
case <-time.After(1500 * time.Millisecond):
|
||||
return fmt.Errorf("timeout")
|
||||
}
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func expectNoEvent(t *testing.T, eventch <-chan ListenerEventType) error {
|
||||
select {
|
||||
case e := <-eventch:
|
||||
return fmt.Errorf("unexpected event %v", e)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return nil
|
||||
}
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func newTestListenerConn(t *testing.T) (*ListenerConn, <-chan *Notification) {
|
||||
datname := os.Getenv("PGDATABASE")
|
||||
sslmode := os.Getenv("PGSSLMODE")
|
||||
|
||||
if datname == "" {
|
||||
os.Setenv("PGDATABASE", "pqgotest")
|
||||
}
|
||||
|
||||
if sslmode == "" {
|
||||
os.Setenv("PGSSLMODE", "disable")
|
||||
}
|
||||
|
||||
notificationChan := make(chan *Notification)
|
||||
l, err := NewListenerConn("", notificationChan)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return l, notificationChan
|
||||
}
|
||||
|
||||
func TestNewListenerConn(t *testing.T) {
|
||||
l, _ := newTestListenerConn(t)
|
||||
|
||||
defer l.Close()
|
||||
}
|
||||
|
||||
func TestConnListen(t *testing.T) {
|
||||
l, channel := newTestListenerConn(t)
|
||||
|
||||
defer l.Close()
|
||||
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
ok, err := l.Listen("notify_test")
|
||||
if !ok || err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNotification(t, channel, "notify_test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnUnlisten(t *testing.T) {
|
||||
l, channel := newTestListenerConn(t)
|
||||
|
||||
defer l.Close()
|
||||
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
ok, err := l.Listen("notify_test")
|
||||
if !ok || err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_test")
|
||||
|
||||
err = expectNotification(t, channel, "notify_test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ok, err = l.Unlisten("notify_test")
|
||||
if !ok || err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNoNotification(t, channel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnUnlistenAll(t *testing.T) {
|
||||
l, channel := newTestListenerConn(t)
|
||||
|
||||
defer l.Close()
|
||||
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
ok, err := l.Listen("notify_test")
|
||||
if !ok || err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_test")
|
||||
|
||||
err = expectNotification(t, channel, "notify_test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ok, err = l.UnlistenAll()
|
||||
if !ok || err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNoNotification(t, channel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnClose(t *testing.T) {
|
||||
l, _ := newTestListenerConn(t)
|
||||
defer l.Close()
|
||||
|
||||
err := l.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = l.Close()
|
||||
if err != errListenerConnClosed {
|
||||
t.Fatalf("expected errListenerConnClosed; got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnPing(t *testing.T) {
|
||||
l, _ := newTestListenerConn(t)
|
||||
defer l.Close()
|
||||
err := l.Ping()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = l.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = l.Ping()
|
||||
if err != errListenerConnClosed {
|
||||
t.Fatalf("expected errListenerConnClosed; got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyExtra(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
if getServerVersion(t, db) < 90000 {
|
||||
t.Log("skipping test due to old PG version")
|
||||
return
|
||||
}
|
||||
|
||||
l, channel := newTestListenerConn(t)
|
||||
defer l.Close()
|
||||
|
||||
ok, err := l.Listen("notify_test")
|
||||
if !ok || err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_test, 'something'")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNotification(t, channel, "notify_test", "something")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// create a new test listener and also set the timeouts
|
||||
func newTestListenerTimeout(t *testing.T, min time.Duration, max time.Duration) (*Listener, <-chan ListenerEventType) {
|
||||
datname := os.Getenv("PGDATABASE")
|
||||
sslmode := os.Getenv("PGSSLMODE")
|
||||
|
||||
if datname == "" {
|
||||
os.Setenv("PGDATABASE", "pqgotest")
|
||||
}
|
||||
|
||||
if sslmode == "" {
|
||||
os.Setenv("PGSSLMODE", "disable")
|
||||
}
|
||||
|
||||
eventch := make(chan ListenerEventType, 16)
|
||||
l := NewListener("", min, max, func(t ListenerEventType, err error) { eventch <- t })
|
||||
err := expectEvent(t, eventch, ListenerEventConnected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return l, eventch
|
||||
}
|
||||
|
||||
func newTestListener(t *testing.T) (*Listener, <-chan ListenerEventType) {
|
||||
return newTestListenerTimeout(t, time.Hour, time.Hour)
|
||||
}
|
||||
|
||||
func TestListenerListen(t *testing.T) {
|
||||
l, _ := newTestListener(t)
|
||||
defer l.Close()
|
||||
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
err := l.Listen("notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNotification(t, l.Notify, "notify_listen_test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerUnlisten(t *testing.T) {
|
||||
l, _ := newTestListener(t)
|
||||
defer l.Close()
|
||||
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
err := l.Listen("notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = l.Unlisten("notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNotification(t, l.Notify, "notify_listen_test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNoNotification(t, l.Notify)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerUnlistenAll(t *testing.T) {
|
||||
l, _ := newTestListener(t)
|
||||
defer l.Close()
|
||||
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
err := l.Listen("notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = l.UnlistenAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNotification(t, l.Notify, "notify_listen_test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNoNotification(t, l.Notify)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerFailedQuery(t *testing.T) {
|
||||
l, eventch := newTestListener(t)
|
||||
defer l.Close()
|
||||
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
err := l.Listen("notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNotification(t, l.Notify, "notify_listen_test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// shouldn't cause a disconnect
|
||||
ok, err := l.cn.ExecSimpleQuery("SELECT error")
|
||||
if !ok {
|
||||
t.Fatalf("could not send query to server: %v", err)
|
||||
}
|
||||
_, ok = err.(PGError)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
err = expectNoEvent(t, eventch)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// should still work
|
||||
_, err = db.Exec("NOTIFY notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNotification(t, l.Notify, "notify_listen_test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerReconnect(t *testing.T) {
|
||||
l, eventch := newTestListenerTimeout(t, 20*time.Millisecond, time.Hour)
|
||||
defer l.Close()
|
||||
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
err := l.Listen("notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec("NOTIFY notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNotification(t, l.Notify, "notify_listen_test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// kill the connection and make sure it comes back up
|
||||
ok, err := l.cn.ExecSimpleQuery("SELECT pg_terminate_backend(pg_backend_pid())")
|
||||
if ok {
|
||||
t.Fatalf("could not kill the connection: %v", err)
|
||||
}
|
||||
if err != io.EOF {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
err = expectEvent(t, eventch, ListenerEventDisconnected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = expectEvent(t, eventch, ListenerEventReconnected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// should still work
|
||||
_, err = db.Exec("NOTIFY notify_listen_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// should get nil after Reconnected
|
||||
err = expectNotification(t, l.Notify, "", "")
|
||||
if err != errNilNotification {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = expectNotification(t, l.Notify, "notify_listen_test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerClose(t *testing.T) {
|
||||
l, _ := newTestListenerTimeout(t, 20*time.Millisecond, time.Hour)
|
||||
defer l.Close()
|
||||
|
||||
err := l.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = l.Close()
|
||||
if err != errListenerClosed {
|
||||
t.Fatalf("expected errListenerClosed; got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerPing(t *testing.T) {
|
||||
l, _ := newTestListenerTimeout(t, 20*time.Millisecond, time.Hour)
|
||||
defer l.Close()
|
||||
|
||||
err := l.Ping()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = l.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = l.Ping()
|
||||
if err != errListenerClosed {
|
||||
t.Fatalf("expected errListenerClosed; got %v", err)
|
||||
}
|
||||
}
|
||||
-245
@@ -1,245 +0,0 @@
|
||||
package pq
|
||||
|
||||
// This file contains SSL tests
|
||||
|
||||
import (
|
||||
_ "crypto/sha256"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func shouldSkipSSLTests(t *testing.T) bool {
|
||||
// Require some special variables for testing certificates
|
||||
if os.Getenv("PQSSLCERTTEST_PATH") == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
value := os.Getenv("PQGOSSLTESTS")
|
||||
if value == "" || value == "0" {
|
||||
return true
|
||||
} else if value == "1" {
|
||||
return false
|
||||
} else {
|
||||
t.Fatalf("unexpected value %q for PQGOSSLTESTS", value)
|
||||
}
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func openSSLConn(t *testing.T, conninfo string) (*sql.DB, error) {
|
||||
db, err := openTestConnConninfo(conninfo)
|
||||
if err != nil {
|
||||
// should never fail
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Do something with the connection to see whether it's working or not.
|
||||
tx, err := db.Begin()
|
||||
if err == nil {
|
||||
return db, tx.Rollback()
|
||||
}
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func checkSSLSetup(t *testing.T, conninfo string) {
|
||||
db, err := openSSLConn(t, conninfo)
|
||||
if err == nil {
|
||||
db.Close()
|
||||
t.Fatal("expected error with conninfo=%q", conninfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect over SSL and run a simple query to test the basics
|
||||
func TestSSLConnection(t *testing.T) {
|
||||
if shouldSkipSSLTests(t) {
|
||||
t.Log("skipping SSL test")
|
||||
return
|
||||
}
|
||||
// Environment sanity check: should fail without SSL
|
||||
checkSSLSetup(t, "sslmode=disable user=pqgossltest")
|
||||
|
||||
db, err := openSSLConn(t, "sslmode=require user=pqgossltest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, err := db.Query("SELECT 1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
// Test sslmode=verify-full
|
||||
func TestSSLVerifyFull(t *testing.T) {
|
||||
if shouldSkipSSLTests(t) {
|
||||
t.Log("skipping SSL test")
|
||||
return
|
||||
}
|
||||
// Environment sanity check: should fail without SSL
|
||||
checkSSLSetup(t, "sslmode=disable user=pqgossltest")
|
||||
|
||||
// Not OK according to the system CA
|
||||
_, err := openSSLConn(t, "host=postgres sslmode=verify-full user=pqgossltest")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
_, ok := err.(x509.UnknownAuthorityError)
|
||||
if !ok {
|
||||
t.Fatalf("expected x509.UnknownAuthorityError, got %#+v", err)
|
||||
}
|
||||
|
||||
rootCertPath := filepath.Join(os.Getenv("PQSSLCERTTEST_PATH"), "root.crt")
|
||||
rootCert := "sslrootcert=" + rootCertPath + " "
|
||||
// No match on Common Name
|
||||
_, err = openSSLConn(t, rootCert + "host=127.0.0.1 sslmode=verify-full user=pqgossltest")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
_, ok = err.(x509.HostnameError)
|
||||
if !ok {
|
||||
t.Fatalf("expected x509.HostnameError, got %#+v", err)
|
||||
}
|
||||
// OK
|
||||
_, err = openSSLConn(t, rootCert + "host=postgres sslmode=verify-full user=pqgossltest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test sslmode=verify-ca
|
||||
func TestSSLVerifyCA(t *testing.T) {
|
||||
if shouldSkipSSLTests(t) {
|
||||
t.Log("skipping SSL test")
|
||||
return
|
||||
}
|
||||
// Environment sanity check: should fail without SSL
|
||||
checkSSLSetup(t, "sslmode=disable user=pqgossltest")
|
||||
|
||||
// Not OK according to the system CA
|
||||
_, err := openSSLConn(t, "host=postgres sslmode=verify-ca user=pqgossltest")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
_, ok := err.(x509.UnknownAuthorityError)
|
||||
if !ok {
|
||||
t.Fatalf("expected x509.UnknownAuthorityError, got %#+v", err)
|
||||
}
|
||||
|
||||
rootCertPath := filepath.Join(os.Getenv("PQSSLCERTTEST_PATH"), "root.crt")
|
||||
rootCert := "sslrootcert=" + rootCertPath + " "
|
||||
// No match on Common Name, but that's OK
|
||||
_, err = openSSLConn(t, rootCert + "host=127.0.0.1 sslmode=verify-ca user=pqgossltest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Everything OK
|
||||
_, err = openSSLConn(t, rootCert + "host=postgres sslmode=verify-ca user=pqgossltest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func getCertConninfo(t *testing.T, source string) string {
|
||||
var sslkey string
|
||||
var sslcert string
|
||||
|
||||
certpath := os.Getenv("PQSSLCERTTEST_PATH")
|
||||
|
||||
switch source {
|
||||
case "missingkey":
|
||||
sslkey = "/tmp/filedoesnotexist"
|
||||
sslcert = filepath.Join(certpath, "postgresql.crt")
|
||||
case "missingcert":
|
||||
sslkey = filepath.Join(certpath, "postgresql.key")
|
||||
sslcert = "/tmp/filedoesnotexist"
|
||||
case "certtwice":
|
||||
sslkey = filepath.Join(certpath, "postgresql.crt")
|
||||
sslcert = filepath.Join(certpath, "postgresql.crt")
|
||||
case "valid":
|
||||
sslkey = filepath.Join(certpath, "postgresql.key")
|
||||
sslcert = filepath.Join(certpath, "postgresql.crt")
|
||||
default:
|
||||
t.Fatalf("invalid source %q", source)
|
||||
}
|
||||
return fmt.Sprintf("sslmode=require user=pqgosslcert sslkey=%s sslcert=%s", sslkey, sslcert)
|
||||
}
|
||||
|
||||
// Authenticate over SSL using client certificates
|
||||
func TestSSLClientCertificates(t *testing.T) {
|
||||
if shouldSkipSSLTests(t) {
|
||||
t.Log("skipping SSL test")
|
||||
return
|
||||
}
|
||||
// Environment sanity check: should fail without SSL
|
||||
checkSSLSetup(t, "sslmode=disable user=pqgossltest")
|
||||
|
||||
// Should also fail without a valid certificate
|
||||
db, err := openSSLConn(t, "sslmode=require user=pqgosslcert")
|
||||
if err == nil {
|
||||
db.Close()
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
pge, ok := err.(*Error)
|
||||
if !ok {
|
||||
t.Fatal("expected pq.Error")
|
||||
}
|
||||
if pge.Code.Name() != "invalid_authorization_specification" {
|
||||
t.Fatalf("unexpected error code %q", pge.Code.Name())
|
||||
}
|
||||
|
||||
// Should work
|
||||
db, err = openSSLConn(t, getCertConninfo(t, "valid"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, err := db.Query("SELECT 1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
// Test errors with ssl certificates
|
||||
func TestSSLClientCertificatesMissingFiles(t *testing.T) {
|
||||
if shouldSkipSSLTests(t) {
|
||||
t.Log("skipping SSL test")
|
||||
return
|
||||
}
|
||||
// Environment sanity check: should fail without SSL
|
||||
checkSSLSetup(t, "sslmode=disable user=pqgossltest")
|
||||
|
||||
// Key missing, should fail
|
||||
_, err := openSSLConn(t, getCertConninfo(t, "missingkey"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
// should be a PathError
|
||||
_, ok := err.(*os.PathError)
|
||||
if !ok {
|
||||
t.Fatalf("expected PathError, got %#+v", err)
|
||||
}
|
||||
|
||||
// Cert missing, should fail
|
||||
_, err = openSSLConn(t, getCertConninfo(t, "missingcert"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
// should be a PathError
|
||||
_, ok = err.(*os.PathError)
|
||||
if !ok {
|
||||
t.Fatalf("expected PathError, got %#+v", err)
|
||||
}
|
||||
|
||||
// Key has wrong permissions, should fail
|
||||
_, err = openSSLConn(t, getCertConninfo(t, "certtwice"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if err != ErrSSLKeyHasWorldPermissions {
|
||||
t.Fatalf("expected ErrSSLKeyHasWorldPermissions, got %#+v", err)
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package pq
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSimpleParseURL(t *testing.T) {
|
||||
expected := "host=hostname.remote"
|
||||
str, err := ParseURL("postgres://hostname.remote")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if str != expected {
|
||||
t.Fatalf("unexpected result from ParseURL:\n+ %v\n- %v", str, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFullParseURL(t *testing.T) {
|
||||
expected := `dbname=database host=hostname.remote password=top\ secret port=1234 user=username`
|
||||
str, err := ParseURL("postgres://username:top%20secret@hostname.remote:1234/database")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if str != expected {
|
||||
t.Fatalf("unexpected result from ParseURL:\n+ %s\n- %s", str, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidProtocolParseURL(t *testing.T) {
|
||||
_, err := ParseURL("http://hostname.remote")
|
||||
switch err {
|
||||
case nil:
|
||||
t.Fatal("Expected an error from parsing invalid protocol")
|
||||
default:
|
||||
msg := "invalid connection protocol: http"
|
||||
if err.Error() != msg {
|
||||
t.Fatalf("Unexpected error message:\n+ %s\n- %s",
|
||||
err.Error(), msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinimalURL(t *testing.T) {
|
||||
cs, err := ParseURL("postgres://")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cs != "" {
|
||||
t.Fatalf("expected blank connection string, got: %q", cs)
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
// Package pq is a pure Go Postgres driver for the database/sql package.
|
||||
|
||||
// +build darwin freebsd linux nacl netbsd openbsd solaris
|
||||
|
||||
package pq
|
||||
|
||||
import "os/user"
|
||||
|
||||
func userCurrent() (string, error) {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.Username, nil
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
Copyright (c) 2014 Dan Sosedoff <dan.sosedoff@gmail.com>
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2016 Dan Sosedoff <dan.sosedoff@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
@@ -1,28 +1,87 @@
|
||||
BINDATA=
|
||||
TARGETS = darwin/amd64 darwin/386 linux/amd64 linux/386 windows/amd64 windows/386
|
||||
GIT_COMMIT = $(shell git rev-parse HEAD)
|
||||
BUILD_TIME = $(shell date -u +"%Y-%m-%dT%H:%M:%SZ" | tr -d '\n')
|
||||
DOCKER_RELEASE_TAG = "sosedoff/pgweb:$(shell git describe --abbrev=0 --tags | sed 's/v//')"
|
||||
DOCKER_LATEST_TAG = "sosedoff/pgweb:latest"
|
||||
BINDATA_IGNORE = $(shell git ls-files -io --exclude-standard $< | sed 's/^/-ignore=/;s/[.]/[.]/g')
|
||||
|
||||
dev: dev-assets
|
||||
godep go build
|
||||
@echo "You can now execute ./pgweb"
|
||||
usage:
|
||||
@echo ""
|
||||
@echo "Task : Description"
|
||||
@echo "----------------- : -------------------"
|
||||
@echo "make setup : Install all necessary dependencies"
|
||||
@echo "make dev : Generate development build"
|
||||
@echo "make test : Run tests"
|
||||
@echo "make build : Generate production build for current OS"
|
||||
@echo "make bootstrap : Install cross-compilation toolchain"
|
||||
@echo "make release : Generate binaries for all supported OSes"
|
||||
@echo "make test : Execute test suite"
|
||||
@echo "make test-all : Execute test suite on multiple PG versions"
|
||||
@echo "make clean : Remove all build files and reset assets"
|
||||
@echo "make assets : Generate production assets file"
|
||||
@echo "make dev-assets : Generate development assets file"
|
||||
@echo "make docker : Build docker image"
|
||||
@echo "make docker-release : Build and tag docker image"
|
||||
@echo "make docker-push : Push docker images to registry"
|
||||
@echo ""
|
||||
|
||||
assets:
|
||||
go-bindata $(BINDATA) -ignore=\\.gitignore -ignore=\\.DS_Store -ignore=\\.gitkeep static/...
|
||||
test:
|
||||
go test -cover ./pkg/...
|
||||
|
||||
test-all:
|
||||
@./script/test_all.sh
|
||||
|
||||
assets: static/
|
||||
go-bindata -o pkg/data/bindata.go -pkg data $(BINDATA_OPTS) $(BINDATA_IGNORE) -ignore=[.]gitignore -ignore=[.]gitkeep $<...
|
||||
|
||||
dev-assets:
|
||||
@$(MAKE) --no-print-directory assets BINDATA="-debug"
|
||||
@$(MAKE) --no-print-directory assets BINDATA_OPTS="-debug"
|
||||
|
||||
dev: dev-assets
|
||||
go build
|
||||
@echo "You can now execute ./pgweb"
|
||||
|
||||
build: assets
|
||||
gox -osarch="darwin/amd64 darwin/386 linux/amd64 linux/386 windows/amd64 windows/386" -output="./bin/pgweb_{{.OS}}_{{.Arch}}"
|
||||
go build
|
||||
@echo "You can now execute ./pgweb"
|
||||
|
||||
release: clean assets
|
||||
@echo "Building binaries..."
|
||||
@gox \
|
||||
-osarch "$(TARGETS)" \
|
||||
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit $(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime $(BUILD_TIME)" \
|
||||
-output "./bin/pgweb_{{.OS}}_{{.Arch}}"
|
||||
|
||||
@echo "Building ARM binaries..."
|
||||
GOOS=linux GOARCH=arm GOARM=5 go build \
|
||||
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit $(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime $(BUILD_TIME)" \
|
||||
-o "./bin/pgweb_linux_arm_v5"
|
||||
|
||||
@echo "\nPackaging binaries...\n"
|
||||
@./script/package.sh
|
||||
|
||||
bootstrap:
|
||||
gox -build-toolchain
|
||||
|
||||
setup:
|
||||
go get github.com/tools/godep
|
||||
godep get github.com/mitchellh/gox
|
||||
godep get github.com/jteeuwen/go-bindata/...
|
||||
go get golang.org/x/tools/cmd/cover
|
||||
go get github.com/mitchellh/gox
|
||||
go get github.com/jteeuwen/go-bindata/...
|
||||
godep restore
|
||||
|
||||
clean:
|
||||
rm -f ./pgweb
|
||||
rm -f ./bin/*
|
||||
rm -f bindata.go
|
||||
@rm -f ./pgweb
|
||||
@rm -rf ./bin/*
|
||||
@rm -f bindata.go
|
||||
|
||||
docker:
|
||||
docker build -t pgweb .
|
||||
docker build -t pgweb .
|
||||
|
||||
docker-release:
|
||||
docker build -t $(DOCKER_RELEASE_TAG) .
|
||||
docker build -t $(DOCKER_LATEST_TAG) .
|
||||
|
||||
docker-push:
|
||||
docker push $(DOCKER_RELEASE_TAG)
|
||||
docker push $(DOCKER_LATEST_TAG)
|
||||
@@ -2,51 +2,46 @@
|
||||
|
||||
Web-based PostgreSQL database browser written in Go.
|
||||
|
||||
[](https://travis-ci.org/sosedoff/pgweb)
|
||||
[](https://github.com/sosedoff/pgweb/releases)
|
||||
[](https://travis-ci.org/sosedoff/pgweb)
|
||||
[](https://ci.appveyor.com/project/sosedoff/pgweb)
|
||||
|
||||
## Overview
|
||||
|
||||
This is a web-based browser for PostgreSQL database server. Its written in Go
|
||||
and works on Mac OSX, Linux and Windows machines. Main idea behind using Go for the backend
|
||||
is to utilize language's ability for cross-compile source code for multiple platforms.
|
||||
This project is an attempt to create a very simple and portable application to work with
|
||||
PostgreSQL databases.
|
||||
Pgweb is a web-based database browser for PostgreSQL, written in Go and works
|
||||
on OSX, Linux and Windows machines. Main idea behind using Go for backend development
|
||||
is to utilize ability of the compiler to produce zero-dependency binaries for
|
||||
multiple platforms. Pgweb was created as an attempt to build very simple and portable
|
||||
application to work with local or remote PostgreSQL databases.
|
||||
|
||||
<img src="screenshots/browse.png" width="345px" />
|
||||
<img src="screenshots/query.png" width="345px" />
|
||||
[See application screenshots](SCREENS.md)
|
||||
|
||||
Features:
|
||||
## Features
|
||||
|
||||
- Connect to local or remote server
|
||||
- Browse tables and table rows
|
||||
- Get table details: structure, size, indices, row count
|
||||
- Execute SQL query and run analyze on it
|
||||
- Export query results to CSV
|
||||
- View query history
|
||||
- Cross-platform support OSX/Linux/Windows 32/64-bit
|
||||
- Simple installation (distributed as a single binary)
|
||||
- Zero dependencies
|
||||
- Works with PostgreSQL 9.1+
|
||||
- SSH Connections
|
||||
- Multiple database sessions
|
||||
- Simple database browser
|
||||
- Execute and analyze custom SQL queries
|
||||
- Table and query data export to CSV/JSON/XML
|
||||
- Query history
|
||||
- Server bookmarks
|
||||
|
||||
Visit [WIKI](https://github.com/sosedoff/pgweb/wiki) for more details
|
||||
|
||||
## Demo
|
||||
|
||||
Visit https://pgweb-demo.herokuapp.com to see pgweb in action.
|
||||
|
||||
## Installation
|
||||
|
||||
Please visit [Github Releases](https://github.com/sosedoff/pgweb/releases) to download a
|
||||
precompiled binary for your operating system.
|
||||
[Precompiled binaries](https://github.com/sosedoff/pgweb/releases) for supported
|
||||
operating systems are available.
|
||||
|
||||
Currently supported:
|
||||
|
||||
- Mac OSX 64bit
|
||||
- Linux 32/64bit
|
||||
- Windows 32/64bit
|
||||
|
||||
Supported PostgreSQL versions:
|
||||
|
||||
- 9.1
|
||||
- 9.2
|
||||
- 9.3
|
||||
|
||||
Older versions of PostgreSQL might also work but this project is not tested on
|
||||
8.x branches.
|
||||
|
||||
## Run on Heroku
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/sosedoff/pgweb)
|
||||
[More installation options](https://github.com/sosedoff/pgweb/wiki/Installation)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -68,104 +63,52 @@ Connection URL scheme is also supported:
|
||||
pgweb --url postgres://user:password@host:port/database?sslmode=[mode]
|
||||
```
|
||||
|
||||
It works great with [Heroku Postgres](https://postgres.heroku.com) if you need
|
||||
to troubleshoot production database or simply run a few queries.
|
||||
### Multiple database sessions
|
||||
|
||||
### CLI options
|
||||
To enable multiple database sessions in pgweb, start the server with:
|
||||
|
||||
```
|
||||
Usage:
|
||||
pgweb [OPTIONS]
|
||||
|
||||
Application Options:
|
||||
-v, --version Print version
|
||||
-d, --debug Enable debugging mode (false)
|
||||
-s, --skip-open Skip browser open on start
|
||||
--url= Database connection string
|
||||
--host= Server hostname or IP (localhost)
|
||||
--port= Server port (5432)
|
||||
--user= Database user (postgres)
|
||||
--pass= Password for user
|
||||
--db= Database name (postgres)
|
||||
--ssl= SSL option (disable)
|
||||
--bind= HTTP server host (localhost)
|
||||
--listen= HTTP server listen port (8080)
|
||||
--auth-user= HTTP basic auth user
|
||||
--auth-pass= HTTP basic auth password
|
||||
pgweb --sessions
|
||||
```
|
||||
|
||||
## Build from source
|
||||
|
||||
Go 1.3+ is required. You can install Go with `homebrew`:
|
||||
Or set environment variable:
|
||||
|
||||
```
|
||||
brew install go
|
||||
SESSIONS=1 pgweb
|
||||
```
|
||||
|
||||
To compile source code run the following command:
|
||||
## Deploy on Heroku
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/sosedoff/pgweb)
|
||||
|
||||
## Testing
|
||||
|
||||
Before running tests, make sure you have PostgreSQL server running on `localhost:5432`
|
||||
interface. Also, you must have `postgres` user that could create new databases
|
||||
in your local environment. Pgweb server should not be running at the same time.
|
||||
|
||||
Execute test suite:
|
||||
|
||||
```
|
||||
make setup
|
||||
make dev
|
||||
make test
|
||||
```
|
||||
|
||||
This will produce `pgweb` binary in the current directory.
|
||||
|
||||
There's also a task to compile binaries for other operating systems:
|
||||
If you're using Docker locally, you might also run pgweb test suite against
|
||||
all supported PostgreSQL version with a single command:
|
||||
|
||||
```
|
||||
make build
|
||||
make test-all
|
||||
```
|
||||
|
||||
Under the hood it uses [gox](https://github.com/mitchellh/gox). Compiled binaries
|
||||
will be stored into `./bin` directory.
|
||||
## Contribute
|
||||
|
||||
|
||||
## Use in Docker
|
||||
|
||||
Build the image:
|
||||
|
||||
```
|
||||
docker build -t pgweb .
|
||||
```
|
||||
|
||||
Start container:
|
||||
|
||||
```
|
||||
docker run [OPTIONS of docker] pgweb [OPTIONS of pgweb]
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
Run postgresql container:
|
||||
|
||||
```
|
||||
docker run -d \
|
||||
--name="postgresql" \
|
||||
-p 5432:5432 \
|
||||
-e USER="testuser" \
|
||||
-e DB="testdb" \
|
||||
-e PASS="test123" \
|
||||
paintedfox/postgresql
|
||||
```
|
||||
|
||||
Run pgweb container:
|
||||
|
||||
```
|
||||
docker run -d \
|
||||
-p 8080:8080 pgweb \
|
||||
--url postgres://testuser:test123@your-ip:5432/testdb \
|
||||
--bind 0.0.0.0
|
||||
```
|
||||
|
||||
Then open [http://your-ip:8082](#) in your browser.
|
||||
|
||||
## Contributing
|
||||
|
||||
- Fork repository
|
||||
- Create a feature or bugfix branch
|
||||
- Open a new pull request
|
||||
- Use github issues for any questions
|
||||
- Fork this repository
|
||||
- Create a new feature branch for a new functionality or bugfix
|
||||
- Commit your changes
|
||||
- Execute test suite
|
||||
- Push your code and open a new pull request
|
||||
- Use [issues](https://github.com/sosedoff/pgweb/issues) for any questions
|
||||
- Check [wiki](https://github.com/sosedoff/pgweb/wiki) for extra documentation
|
||||
|
||||
## Contact
|
||||
|
||||
@@ -177,4 +120,4 @@ Then open [http://your-ip:8082](#) in your browser.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Dan Sosedoff, <dan.sosedoff@gmail.com>
|
||||
Copyright (c) 2014-2016 Dan Sosedoff, <dan.sosedoff@gmail.com>
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# Screenshots
|
||||
|
||||
### Connect
|
||||
<img src="screenshots/connect.png" />
|
||||
|
||||
### Browse table rows
|
||||
<img src="screenshots/browse.png" />
|
||||
|
||||
### Write SQL queries
|
||||
<img src="screenshots/query.png" />
|
||||
@@ -1,213 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var MIME_TYPES = map[string]string{
|
||||
".css": "text/css",
|
||||
".js": "application/javascript",
|
||||
".icon": "image-x-icon",
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Message string `json:"error"`
|
||||
}
|
||||
|
||||
func assetContentType(name string) string {
|
||||
mime := MIME_TYPES[filepath.Ext(name)]
|
||||
|
||||
if mime != "" {
|
||||
return mime
|
||||
} else {
|
||||
return "text/plain"
|
||||
}
|
||||
}
|
||||
|
||||
func API_Home(c *gin.Context) {
|
||||
data, err := Asset("static/index.html")
|
||||
|
||||
if err != nil {
|
||||
c.String(400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(200, "text/html; charset=utf-8", data)
|
||||
}
|
||||
|
||||
func API_Connect(c *gin.Context) {
|
||||
url := c.Request.FormValue("url")
|
||||
|
||||
if url == "" {
|
||||
c.JSON(400, Error{"Url parameter is required"})
|
||||
return
|
||||
}
|
||||
|
||||
client, err := NewClientFromUrl(url)
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err = client.Test()
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
info, err := client.Info()
|
||||
|
||||
if err == nil {
|
||||
if dbClient != nil {
|
||||
dbClient.db.Close()
|
||||
}
|
||||
|
||||
dbClient = client
|
||||
}
|
||||
|
||||
c.JSON(200, info.Format()[0])
|
||||
}
|
||||
|
||||
func API_GetDatabases(c *gin.Context) {
|
||||
names, err := dbClient.Databases()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, names)
|
||||
}
|
||||
|
||||
func API_RunQuery(c *gin.Context) {
|
||||
query := strings.TrimSpace(c.Request.FormValue("query"))
|
||||
|
||||
if query == "" {
|
||||
c.JSON(400, errors.New("Query parameter is missing"))
|
||||
return
|
||||
}
|
||||
|
||||
API_HandleQuery(query, c)
|
||||
}
|
||||
|
||||
func API_ExplainQuery(c *gin.Context) {
|
||||
query := strings.TrimSpace(c.Request.FormValue("query"))
|
||||
|
||||
if query == "" {
|
||||
c.JSON(400, errors.New("Query parameter is missing"))
|
||||
return
|
||||
}
|
||||
|
||||
API_HandleQuery(fmt.Sprintf("EXPLAIN ANALYZE %s", query), c)
|
||||
}
|
||||
|
||||
func API_GetTables(c *gin.Context) {
|
||||
names, err := dbClient.Tables()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, names)
|
||||
}
|
||||
|
||||
func API_GetTable(c *gin.Context) {
|
||||
res, err := dbClient.Table(c.Params.ByName("table"))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res)
|
||||
}
|
||||
|
||||
func API_GetTableInfo(c *gin.Context) {
|
||||
res, err := dbClient.TableInfo(c.Params.ByName("table"))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res.Format()[0])
|
||||
}
|
||||
|
||||
func API_History(c *gin.Context) {
|
||||
c.JSON(200, dbClient.history)
|
||||
}
|
||||
|
||||
func API_Info(c *gin.Context) {
|
||||
if dbClient == nil {
|
||||
c.JSON(400, Error{"Not connected"})
|
||||
return
|
||||
}
|
||||
|
||||
res, err := dbClient.Info()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res.Format()[0])
|
||||
}
|
||||
|
||||
func API_TableIndexes(c *gin.Context) {
|
||||
res, err := dbClient.TableIndexes(c.Params.ByName("table"))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res)
|
||||
}
|
||||
|
||||
func API_HandleQuery(query string, c *gin.Context) {
|
||||
result, err := dbClient.Query(query)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
q := c.Request.URL.Query()
|
||||
|
||||
if len(q["format"]) > 0 {
|
||||
if q["format"][0] == "csv" {
|
||||
c.Data(200, "text/csv", result.CSV())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
|
||||
func API_ServeAsset(c *gin.Context) {
|
||||
file := fmt.Sprintf(
|
||||
"static/%s/%s",
|
||||
c.Params.ByName("type"),
|
||||
c.Params.ByName("name"),
|
||||
)
|
||||
|
||||
data, err := Asset(file)
|
||||
|
||||
if err != nil {
|
||||
c.String(400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
c.String(404, "Asset is empty")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(200, assetContentType(file), data)
|
||||
}
|
||||
@@ -8,20 +8,20 @@
|
||||
"keywords": [
|
||||
"postgres",
|
||||
"database",
|
||||
"golang",
|
||||
"frontend"
|
||||
],
|
||||
"addons": [
|
||||
"heroku-postgresql"
|
||||
],
|
||||
"env": {
|
||||
"BUILDPACK_URL": "https://github.com/kr/heroku-buildpack-go",
|
||||
"AUTH_USER": {
|
||||
"description": "A username to log into pgweb.",
|
||||
"value": "admin"
|
||||
},
|
||||
"AUTH_PASS": {
|
||||
"description": "The login password. Please change this.",
|
||||
"value": "pgweb"
|
||||
"generator": "secret"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
clone_folder: c:\go\src\github.com\sosedoff\pgweb
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
|
||||
services:
|
||||
- postgresql
|
||||
|
||||
install:
|
||||
- set PATH=%GOPATH%\bin;%PATH%
|
||||
- echo %PATH%
|
||||
- echo %GOPATH%
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
- go env
|
||||
- go version
|
||||
- go get github.com/tools/godep
|
||||
- godep get github.com/mitchellh/gox
|
||||
- godep get github.com/jteeuwen/go-bindata/...
|
||||
- godep restore
|
||||
|
||||
build_script:
|
||||
- godep go build
|
||||
|
||||
test_script:
|
||||
- godep go test -cover ./...
|
||||
|
||||
deploy: off
|
||||
-15176
File diff suppressed because it is too large
Load Diff
@@ -1,195 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
db *sqlx.DB
|
||||
history []string
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Columns []string `json:"columns"`
|
||||
Rows [][]interface{} `json:"rows"`
|
||||
}
|
||||
|
||||
func NewError(err error) Error {
|
||||
return Error{err.Error()}
|
||||
}
|
||||
|
||||
func NewClient() (*Client, error) {
|
||||
db, err := sqlx.Open("postgres", getConnectionString())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{db: db}, nil
|
||||
}
|
||||
|
||||
func NewClientFromUrl(url string) (*Client, error) {
|
||||
db, err := sqlx.Open("postgres", url)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{db: db}, nil
|
||||
}
|
||||
|
||||
func (client *Client) Test() error {
|
||||
return client.db.Ping()
|
||||
}
|
||||
|
||||
func (client *Client) recordQuery(query string) {
|
||||
client.history = append(client.history, query)
|
||||
}
|
||||
|
||||
func (client *Client) Info() (*Result, error) {
|
||||
return client.Query(PG_INFO)
|
||||
}
|
||||
|
||||
func (client *Client) Databases() ([]string, error) {
|
||||
res, err := client.Query(PG_DATABASES)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tables []string
|
||||
|
||||
for _, row := range res.Rows {
|
||||
tables = append(tables, row[0].(string))
|
||||
}
|
||||
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func (client *Client) Tables() ([]string, error) {
|
||||
res, err := client.Query(PG_TABLES)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tables []string
|
||||
|
||||
for _, row := range res.Rows {
|
||||
tables = append(tables, row[0].(string))
|
||||
}
|
||||
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func (client *Client) Table(table string) (*Result, error) {
|
||||
return client.Query(fmt.Sprintf(PG_TABLE_SCHEMA, table))
|
||||
}
|
||||
|
||||
func (client *Client) TableInfo(table string) (*Result, error) {
|
||||
return client.Query(fmt.Sprintf(PG_TABLE_INFO, table, table, table, table))
|
||||
}
|
||||
|
||||
func (client *Client) TableIndexes(table string) (*Result, error) {
|
||||
res, err := client.Query(fmt.Sprintf(PG_TABLE_INDEXES, table))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (client *Client) Query(query string) (*Result, error) {
|
||||
rows, err := client.db.Queryx(query)
|
||||
|
||||
client.recordQuery(query)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
cols, err := rows.Columns()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := Result{
|
||||
Columns: cols,
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
obj, err := rows.SliceScan()
|
||||
|
||||
for i, item := range obj {
|
||||
if item == nil {
|
||||
obj[i] = nil
|
||||
} else {
|
||||
t := reflect.TypeOf(item).Kind().String()
|
||||
|
||||
if t == "slice" {
|
||||
obj[i] = string(item.([]byte))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
result.Rows = append(result.Rows, obj)
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (res *Result) Format() []map[string]interface{} {
|
||||
var items []map[string]interface{}
|
||||
|
||||
for _, row := range res.Rows {
|
||||
item := make(map[string]interface{})
|
||||
|
||||
for i, c := range res.Columns {
|
||||
item[c] = row[i]
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (res *Result) CSV() []byte {
|
||||
buff := &bytes.Buffer{}
|
||||
writer := csv.NewWriter(buff)
|
||||
|
||||
writer.Write(res.Columns)
|
||||
|
||||
for _, row := range res.Rows {
|
||||
record := make([]string, len(res.Columns))
|
||||
|
||||
for i, item := range row {
|
||||
if item != nil {
|
||||
record[i] = fmt.Sprintf("%v", item)
|
||||
} else {
|
||||
record[i] = ""
|
||||
}
|
||||
}
|
||||
|
||||
err := writer.Write(record)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
writer.Flush()
|
||||
return buff.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
host = "localhost"
|
||||
port = "5432"
|
||||
user = "postgres"
|
||||
database = "mydatabase"
|
||||
ssl = "disable"
|
||||
@@ -0,0 +1 @@
|
||||
url = "postgres://username:password@host:port/database?sslmode=disable"
|
||||
+1871
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
invalid encoding
|
||||
@@ -0,0 +1,5 @@
|
||||
host = "localhost"
|
||||
port = 5432
|
||||
user = "postgres"
|
||||
database = "mydatabase"
|
||||
ssl = "disable"
|
||||
@@ -5,116 +5,75 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jessevdk/go-flags"
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/api"
|
||||
"github.com/sosedoff/pgweb/pkg/client"
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/connection"
|
||||
"github.com/sosedoff/pgweb/pkg/util"
|
||||
)
|
||||
|
||||
const VERSION = "0.4.0"
|
||||
|
||||
var options struct {
|
||||
Version bool `short:"v" long:"version" description:"Print version"`
|
||||
Debug bool `short:"d" long:"debug" description:"Enable debugging mode" default:"false"`
|
||||
Url string `long:"url" description:"Database connection string"`
|
||||
Host string `long:"host" description:"Server hostname or IP"`
|
||||
Port int `long:"port" description:"Server port" default:"5432"`
|
||||
User string `long:"user" description:"Database user"`
|
||||
Pass string `long:"pass" description:"Password for user"`
|
||||
DbName string `long:"db" description:"Database name"`
|
||||
Ssl string `long:"ssl" description:"SSL option" default:"disable"`
|
||||
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
|
||||
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8080"`
|
||||
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
|
||||
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
|
||||
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
|
||||
}
|
||||
|
||||
var dbClient *Client
|
||||
var options command.Options
|
||||
|
||||
func exitWithMessage(message string) {
|
||||
fmt.Println("Error:", message)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func getConnectionString() string {
|
||||
if options.Url != "" {
|
||||
url := options.Url
|
||||
|
||||
if strings.Contains(url, "postgresql://") {
|
||||
fmt.Println("Invalid URL format. It should match: postgres://user:password@host:port/db?sslmode=mode")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if options.Ssl != "" && !strings.Contains(url, "sslmode") {
|
||||
url += fmt.Sprintf("?sslmode=%s", options.Ssl)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
str := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s dbname=%s sslmode=%s",
|
||||
options.Host, options.Port,
|
||||
options.User, options.DbName,
|
||||
options.Ssl,
|
||||
)
|
||||
|
||||
if options.Pass != "" {
|
||||
str += fmt.Sprintf(" password=%s", options.Pass)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func connectionSettingsBlank() bool {
|
||||
return options.Host == "" &&
|
||||
options.User == "" &&
|
||||
options.DbName == "" &&
|
||||
options.Url == ""
|
||||
}
|
||||
|
||||
func initClient() {
|
||||
if connectionSettingsBlank() {
|
||||
if connection.IsBlank(command.Opts) {
|
||||
return
|
||||
}
|
||||
|
||||
client, err := NewClient()
|
||||
cl, err := client.New()
|
||||
if err != nil {
|
||||
exitWithMessage(err.Error())
|
||||
}
|
||||
|
||||
if command.Opts.Debug {
|
||||
fmt.Println("Server connection string:", cl.ConnectionString)
|
||||
}
|
||||
|
||||
fmt.Println("Connecting to server...")
|
||||
err = client.Test()
|
||||
err = cl.Test()
|
||||
if err != nil {
|
||||
exitWithMessage(err.Error())
|
||||
}
|
||||
|
||||
fmt.Println("Checking tables...")
|
||||
_, err = client.Tables()
|
||||
fmt.Println("Checking database objects...")
|
||||
_, err = cl.Objects()
|
||||
if err != nil {
|
||||
exitWithMessage(err.Error())
|
||||
}
|
||||
|
||||
dbClient = client
|
||||
api.DbClient = cl
|
||||
}
|
||||
|
||||
func initOptions() {
|
||||
_, err := flags.ParseArgs(&options, os.Args)
|
||||
|
||||
err := command.ParseOptions()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if options.Url == "" {
|
||||
options.Url = os.Getenv("DATABASE_URL")
|
||||
}
|
||||
options = command.Opts
|
||||
|
||||
if options.Version {
|
||||
fmt.Printf("pgweb v%s\n", VERSION)
|
||||
printVersion()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
printVersion()
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
str := fmt.Sprintf("Pgweb v%s", command.VERSION)
|
||||
if command.GitCommit != "" {
|
||||
str += fmt.Sprintf(" (git: %s)", command.GitCommit)
|
||||
}
|
||||
|
||||
fmt.Println(str)
|
||||
}
|
||||
|
||||
func startServer() {
|
||||
@@ -126,23 +85,16 @@ func startServer() {
|
||||
router.Use(gin.BasicAuth(auth))
|
||||
}
|
||||
|
||||
router.GET("/", API_Home)
|
||||
router.POST("/connect", API_Connect)
|
||||
router.GET("/databases", API_GetDatabases)
|
||||
router.GET("/info", API_Info)
|
||||
router.GET("/tables", API_GetTables)
|
||||
router.GET("/tables/:table", API_GetTable)
|
||||
router.GET("/tables/:table/info", API_GetTableInfo)
|
||||
router.GET("/tables/:table/indexes", API_TableIndexes)
|
||||
router.GET("/query", API_RunQuery)
|
||||
router.POST("/query", API_RunQuery)
|
||||
router.GET("/explain", API_ExplainQuery)
|
||||
router.POST("/explain", API_ExplainQuery)
|
||||
router.GET("/history", API_History)
|
||||
router.GET("/static/:type/:name", API_ServeAsset)
|
||||
api.SetupRoutes(router)
|
||||
|
||||
fmt.Println("Starting server...")
|
||||
go router.Run(fmt.Sprintf("%v:%v", options.HttpHost, options.HttpPort))
|
||||
go func() {
|
||||
err := router.Run(fmt.Sprintf("%v:%v", options.HttpHost, options.HttpPort))
|
||||
if err != nil {
|
||||
fmt.Println("Cant start server:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func handleSignals() {
|
||||
@@ -152,7 +104,7 @@ func handleSignals() {
|
||||
}
|
||||
|
||||
func openPage() {
|
||||
url := fmt.Sprintf("http://%v:%v", options.HttpHost, options.HttpPort)
|
||||
url := fmt.Sprintf("http://%v:%v/%s", options.HttpHost, options.HttpPort, options.Prefix)
|
||||
fmt.Println("To view database open", url, "in browser")
|
||||
|
||||
if options.SkipOpen {
|
||||
@@ -171,14 +123,19 @@ func main() {
|
||||
initOptions()
|
||||
initClient()
|
||||
|
||||
if dbClient != nil {
|
||||
defer dbClient.db.Close()
|
||||
if api.DbClient != nil {
|
||||
defer api.DbClient.Close()
|
||||
}
|
||||
|
||||
if !options.Debug {
|
||||
gin.SetMode("release")
|
||||
}
|
||||
|
||||
// Print memory usage every 30 seconds with debug flag
|
||||
if options.Debug {
|
||||
util.StartProfiler()
|
||||
}
|
||||
|
||||
startServer()
|
||||
openPage()
|
||||
handleSignals()
|
||||
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/bookmarks"
|
||||
"github.com/sosedoff/pgweb/pkg/client"
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/connection"
|
||||
"github.com/sosedoff/pgweb/pkg/shared"
|
||||
)
|
||||
|
||||
var (
|
||||
DbClient *client.Client
|
||||
DbSessions = map[string]*client.Client{}
|
||||
)
|
||||
|
||||
func DB(c *gin.Context) *client.Client {
|
||||
if command.Opts.Sessions {
|
||||
return DbSessions[getSessionId(c.Request)]
|
||||
} else {
|
||||
return DbClient
|
||||
}
|
||||
}
|
||||
|
||||
func setClient(c *gin.Context, newClient *client.Client) error {
|
||||
currentClient := DB(c)
|
||||
if currentClient != nil {
|
||||
currentClient.Close()
|
||||
}
|
||||
|
||||
if !command.Opts.Sessions {
|
||||
DbClient = newClient
|
||||
return nil
|
||||
}
|
||||
|
||||
sessionId := getSessionId(c.Request)
|
||||
if sessionId == "" {
|
||||
return errors.New("Session ID is required")
|
||||
}
|
||||
|
||||
DbSessions[sessionId] = newClient
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetHome(c *gin.Context) {
|
||||
serveStaticAsset("/index.html", c)
|
||||
}
|
||||
|
||||
func GetAsset(c *gin.Context) {
|
||||
serveStaticAsset(c.Params.ByName("path"), c)
|
||||
}
|
||||
|
||||
func GetSessions(c *gin.Context) {
|
||||
// In debug mode endpoint will return a lot of sensitive information
|
||||
// like full database connection string and all query history.
|
||||
if command.Opts.Debug {
|
||||
c.JSON(200, DbSessions)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, map[string]int{"sessions": len(DbSessions)})
|
||||
}
|
||||
|
||||
func Connect(c *gin.Context) {
|
||||
var sshInfo *shared.SSHInfo
|
||||
url := c.Request.FormValue("url")
|
||||
|
||||
if url == "" {
|
||||
c.JSON(400, Error{"Url parameter is required"})
|
||||
return
|
||||
}
|
||||
|
||||
opts := command.Options{Url: url}
|
||||
url, err := connection.FormatUrl(opts)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if c.Request.FormValue("ssh") != "" {
|
||||
sshInfo = parseSshInfo(c)
|
||||
}
|
||||
|
||||
cl, err := client.NewFromUrl(url, sshInfo)
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err = cl.Test()
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
info, err := cl.Info()
|
||||
if err == nil {
|
||||
err = setClient(c, cl)
|
||||
if err != nil {
|
||||
cl.Close()
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, info.Format()[0])
|
||||
}
|
||||
|
||||
func Disconnect(c *gin.Context) {
|
||||
conn := DB(c)
|
||||
|
||||
if conn == nil {
|
||||
c.JSON(400, Error{"Not connected"})
|
||||
return
|
||||
}
|
||||
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func GetDatabases(c *gin.Context) {
|
||||
names, err := DB(c).Databases()
|
||||
serveResult(names, err, c)
|
||||
}
|
||||
|
||||
func GetObjects(c *gin.Context) {
|
||||
result, err := DB(c).Objects()
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
objects := client.ObjectsFromResult(result)
|
||||
c.JSON(200, objects)
|
||||
}
|
||||
|
||||
func RunQuery(c *gin.Context) {
|
||||
query := cleanQuery(c.Request.FormValue("query"))
|
||||
|
||||
if query == "" {
|
||||
c.JSON(400, NewError(errors.New("Query parameter is missing")))
|
||||
return
|
||||
}
|
||||
|
||||
HandleQuery(query, c)
|
||||
}
|
||||
|
||||
func ExplainQuery(c *gin.Context) {
|
||||
query := cleanQuery(c.Request.FormValue("query"))
|
||||
|
||||
if query == "" {
|
||||
c.JSON(400, NewError(errors.New("Query parameter is missing")))
|
||||
return
|
||||
}
|
||||
|
||||
HandleQuery(fmt.Sprintf("EXPLAIN ANALYZE %s", query), c)
|
||||
}
|
||||
|
||||
func GetSchemas(c *gin.Context) {
|
||||
res, err := DB(c).Schemas()
|
||||
serveResult(res, err, c)
|
||||
}
|
||||
|
||||
func GetTable(c *gin.Context) {
|
||||
var res *client.Result
|
||||
var err error
|
||||
|
||||
if c.Request.FormValue("type") == "materialized_view" {
|
||||
res, err = DB(c).MaterializedView(c.Params.ByName("table"))
|
||||
} else {
|
||||
res, err = DB(c).Table(c.Params.ByName("table"))
|
||||
}
|
||||
|
||||
serveResult(res, err, c)
|
||||
}
|
||||
|
||||
func GetTableRows(c *gin.Context) {
|
||||
offset, err := parseIntFormValue(c, "offset", 0)
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
limit, err := parseIntFormValue(c, "limit", 100)
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
opts := client.RowsOptions{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
SortColumn: c.Request.FormValue("sort_column"),
|
||||
SortOrder: c.Request.FormValue("sort_order"),
|
||||
Where: c.Request.FormValue("where"),
|
||||
}
|
||||
|
||||
res, err := DB(c).TableRows(c.Params.ByName("table"), opts)
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
countRes, err := DB(c).TableRowsCount(c.Params.ByName("table"), opts)
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
numFetch := int64(opts.Limit)
|
||||
numOffset := int64(opts.Offset)
|
||||
numRows := countRes.Rows[0][0].(int64)
|
||||
numPages := numRows / numFetch
|
||||
|
||||
if numPages*numFetch < numRows {
|
||||
numPages++
|
||||
}
|
||||
|
||||
res.Pagination = &client.Pagination{
|
||||
Rows: numRows,
|
||||
Page: (numOffset / numFetch) + 1,
|
||||
Pages: numPages,
|
||||
PerPage: numFetch,
|
||||
}
|
||||
|
||||
serveResult(res, err, c)
|
||||
}
|
||||
|
||||
func GetTableInfo(c *gin.Context) {
|
||||
res, err := DB(c).TableInfo(c.Params.ByName("table"))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res.Format()[0])
|
||||
}
|
||||
|
||||
func GetHistory(c *gin.Context) {
|
||||
c.JSON(200, DB(c).History)
|
||||
}
|
||||
|
||||
func GetConnectionInfo(c *gin.Context) {
|
||||
res, err := DB(c).Info()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res.Format()[0])
|
||||
}
|
||||
|
||||
func GetActivity(c *gin.Context) {
|
||||
res, err := DB(c).Activity()
|
||||
serveResult(res, err, c)
|
||||
}
|
||||
|
||||
func GetTableIndexes(c *gin.Context) {
|
||||
res, err := DB(c).TableIndexes(c.Params.ByName("table"))
|
||||
serveResult(res, err, c)
|
||||
}
|
||||
|
||||
func GetTableConstraints(c *gin.Context) {
|
||||
res, err := DB(c).TableConstraints(c.Params.ByName("table"))
|
||||
serveResult(res, err, c)
|
||||
}
|
||||
|
||||
func HandleQuery(query string, c *gin.Context) {
|
||||
rawQuery, err := base64.StdEncoding.DecodeString(desanitize64(query))
|
||||
if err == nil {
|
||||
query = string(rawQuery)
|
||||
}
|
||||
|
||||
result, err := DB(c).Query(query)
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
format := getQueryParam(c, "format")
|
||||
filename := getQueryParam(c, "filename")
|
||||
|
||||
if filename == "" {
|
||||
filename = fmt.Sprintf("pgweb-%v.%v", time.Now().Unix(), format)
|
||||
}
|
||||
|
||||
if format != "" {
|
||||
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
|
||||
}
|
||||
|
||||
switch format {
|
||||
case "csv":
|
||||
c.Data(200, "text/csv", result.CSV())
|
||||
case "json":
|
||||
c.Data(200, "applicaiton/json", result.JSON())
|
||||
case "xml":
|
||||
c.XML(200, result)
|
||||
default:
|
||||
c.JSON(200, result)
|
||||
}
|
||||
}
|
||||
|
||||
func GetBookmarks(c *gin.Context) {
|
||||
bookmarks, err := bookmarks.ReadAll(bookmarks.Path())
|
||||
serveResult(bookmarks, err, c)
|
||||
}
|
||||
|
||||
func GetInfo(c *gin.Context) {
|
||||
info := map[string]string{
|
||||
"version": command.VERSION,
|
||||
"git_sha": command.GitCommit,
|
||||
"build_time": command.BuildTime,
|
||||
}
|
||||
|
||||
c.JSON(200, info)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_assetContentType(t *testing.T) {
|
||||
samples := map[string]string{
|
||||
"foo.html": "text/html; charset=utf-8",
|
||||
"foo.css": "text/css; charset=utf-8",
|
||||
"foo.js": "application/javascript",
|
||||
"foo.icon": "image-x-icon",
|
||||
"foo.png": "image/png",
|
||||
"foo.jpg": "image/jpeg",
|
||||
"foo.gif": "image/gif",
|
||||
"foo.eot": "application/vnd.ms-fontobject",
|
||||
"foo.svg": "image/svg+xml",
|
||||
"foo.ttf": "application/x-font-ttf",
|
||||
"foo.woff": "application/x-font-woff",
|
||||
"foo.foo": "text/plain; charset=utf-8",
|
||||
"foo": "text/plain; charset=utf-8",
|
||||
}
|
||||
|
||||
for name, expected := range samples {
|
||||
assert.Equal(t, expected, assetContentType(name))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/shared"
|
||||
)
|
||||
|
||||
var extraMimeTypes = map[string]string{
|
||||
".icon": "image-x-icon",
|
||||
".ttf": "application/x-font-ttf",
|
||||
".woff": "application/x-font-woff",
|
||||
".eot": "application/vnd.ms-fontobject",
|
||||
".svg": "image/svg+xml",
|
||||
".html": "text/html; charset-utf-8",
|
||||
}
|
||||
|
||||
// Paths that dont require database connection
|
||||
var allowedPaths = map[string]bool{
|
||||
"/api/sessions": true,
|
||||
"/api/info": true,
|
||||
"/api/connect": true,
|
||||
"/api/bookmarks": true,
|
||||
"/api/history": true,
|
||||
}
|
||||
|
||||
// List of characters replaced by javascript code to make queries url-safe.
|
||||
var base64subs = map[string]string{
|
||||
"-": "+",
|
||||
"_": "/",
|
||||
".": "=",
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Message string `json:"error"`
|
||||
}
|
||||
|
||||
func NewError(err error) Error {
|
||||
return Error{err.Error()}
|
||||
}
|
||||
|
||||
// Returns a clean query without any comment statements
|
||||
func cleanQuery(query string) string {
|
||||
lines := []string{}
|
||||
|
||||
for _, line := range strings.Split(query, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "--") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func desanitize64(query string) string {
|
||||
// Before feeding the string into decoded, we must "reconstruct" the base64 data.
|
||||
// Javascript replaces a few characters to be url-safe.
|
||||
for olds, news := range base64subs {
|
||||
query = strings.Replace(query, olds, news, -1)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func getSessionId(req *http.Request) string {
|
||||
id := req.Header.Get("x-session-id")
|
||||
if id == "" {
|
||||
id = req.URL.Query().Get("_session_id")
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func getQueryParam(c *gin.Context, name string) string {
|
||||
result := ""
|
||||
q := c.Request.URL.Query()
|
||||
|
||||
if len(q[name]) > 0 {
|
||||
result = q[name][0]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func parseIntFormValue(c *gin.Context, name string, defValue int) (int, error) {
|
||||
val := c.Request.FormValue(name)
|
||||
|
||||
if val == "" {
|
||||
return defValue, nil
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return defValue, fmt.Errorf("%s must be a number", name)
|
||||
}
|
||||
|
||||
if num < 1 && defValue != 0 {
|
||||
return defValue, fmt.Errorf("%s must be greater than 0", name)
|
||||
}
|
||||
|
||||
return num, nil
|
||||
}
|
||||
|
||||
func parseSshInfo(c *gin.Context) *shared.SSHInfo {
|
||||
info := shared.SSHInfo{
|
||||
Host: c.Request.FormValue("ssh_host"),
|
||||
Port: c.Request.FormValue("ssh_port"),
|
||||
User: c.Request.FormValue("ssh_user"),
|
||||
Password: c.Request.FormValue("ssh_password"),
|
||||
}
|
||||
|
||||
if info.Port == "" {
|
||||
info.Port = "22"
|
||||
}
|
||||
|
||||
return &info
|
||||
}
|
||||
|
||||
func assetContentType(name string) string {
|
||||
ext := filepath.Ext(name)
|
||||
result := mime.TypeByExtension(ext)
|
||||
|
||||
if result == "" {
|
||||
result = extraMimeTypes[ext]
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
result = "text/plain; charset=utf-8"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_desanitize64(t *testing.T) {
|
||||
examples := map[string]string{
|
||||
"test": "test",
|
||||
"test+test+": "test-test-",
|
||||
"test/test/": "test_test_",
|
||||
"test=test==": "test.test..",
|
||||
}
|
||||
|
||||
for expected, example := range examples {
|
||||
assert.Equal(t, expected, desanitize64(example))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_cleanQuery(t *testing.T) {
|
||||
assert.Equal(t, "a\nb\nc", cleanQuery("a\nb\nc"))
|
||||
assert.Equal(t, "", cleanQuery("--something"))
|
||||
assert.Equal(t, "test", cleanQuery("--test\ntest\n -- test\n"))
|
||||
}
|
||||
|
||||
func Test_getSessionId(t *testing.T) {
|
||||
req := &http.Request{Header: http.Header{}}
|
||||
req.Header.Add("x-session-id", "token")
|
||||
assert.Equal(t, "token", getSessionId(req))
|
||||
|
||||
req = &http.Request{}
|
||||
req.URL, _ = url.Parse("http://foobar/?_session_id=token")
|
||||
assert.Equal(t, "token", getSessionId(req))
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/data"
|
||||
)
|
||||
|
||||
// Middleware function to check database connection status before running queries
|
||||
func dbCheckMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := strings.Replace(c.Request.URL.Path, command.Opts.Prefix, "", -1)
|
||||
|
||||
if allowedPaths[path] == true {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// We dont care about sessions unless they're enabled
|
||||
if !command.Opts.Sessions {
|
||||
if DbClient == nil {
|
||||
c.JSON(400, Error{"Not connected"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
sessionId := getSessionId(c.Request)
|
||||
if sessionId == "" {
|
||||
c.JSON(400, Error{"Session ID is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
conn := DbSessions[sessionId]
|
||||
if conn == nil {
|
||||
c.JSON(400, Error{"Not connected"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware function to print out request parameters and body for debugging
|
||||
func requestInspectMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
log.Println("Request params:", err, c.Request.Form)
|
||||
}
|
||||
}
|
||||
|
||||
func serveStaticAsset(path string, c *gin.Context) {
|
||||
data, err := data.Asset("static" + path)
|
||||
if err != nil {
|
||||
c.String(400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(200, assetContentType(path), data)
|
||||
}
|
||||
|
||||
func serveResult(result interface{}, err error, c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
)
|
||||
|
||||
func SetupMiddlewares(group *gin.RouterGroup) {
|
||||
if command.Opts.Debug {
|
||||
group.Use(requestInspectMiddleware())
|
||||
}
|
||||
|
||||
group.Use(dbCheckMiddleware())
|
||||
}
|
||||
|
||||
func SetupRoutes(router *gin.Engine) {
|
||||
group := router.Group(command.Opts.Prefix)
|
||||
|
||||
group.GET("/", GetHome)
|
||||
group.GET("/static/*path", GetAsset)
|
||||
|
||||
api := group.Group("/api")
|
||||
{
|
||||
SetupMiddlewares(api)
|
||||
|
||||
if command.Opts.Sessions {
|
||||
api.GET("/sessions", GetSessions)
|
||||
}
|
||||
|
||||
api.GET("/info", GetInfo)
|
||||
api.POST("/connect", Connect)
|
||||
api.POST("/disconnect", Disconnect)
|
||||
api.GET("/databases", GetDatabases)
|
||||
api.GET("/connection", GetConnectionInfo)
|
||||
api.GET("/activity", GetActivity)
|
||||
api.GET("/schemas", GetSchemas)
|
||||
api.GET("/objects", GetObjects)
|
||||
api.GET("/tables/:table", GetTable)
|
||||
api.GET("/tables/:table/rows", GetTableRows)
|
||||
api.GET("/tables/:table/info", GetTableInfo)
|
||||
api.GET("/tables/:table/indexes", GetTableIndexes)
|
||||
api.GET("/tables/:table/constraints", GetTableConstraints)
|
||||
api.GET("/query", RunQuery)
|
||||
api.POST("/query", RunQuery)
|
||||
api.GET("/explain", ExplainQuery)
|
||||
api.POST("/explain", ExplainQuery)
|
||||
api.GET("/history", GetHistory)
|
||||
api.GET("/bookmarks", GetBookmarks)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package bookmarks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/shared"
|
||||
)
|
||||
|
||||
type Bookmark struct {
|
||||
Url string `json:"url"` // Postgres connection URL
|
||||
Host string `json:"host"` // Server hostname
|
||||
Port string `json:"port"` // Server port
|
||||
User string `json:"user"` // Database user
|
||||
Password string `json:"password"` // User password
|
||||
Database string `json:"database"` // Database name
|
||||
Ssl string `json:"ssl"` // Connection SSL mode
|
||||
Ssh shared.SSHInfo `json:"ssh"` // SSH tunnel config
|
||||
}
|
||||
|
||||
func readServerConfig(path string) (Bookmark, error) {
|
||||
bookmark := Bookmark{}
|
||||
|
||||
buff, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return bookmark, err
|
||||
}
|
||||
|
||||
_, err = toml.Decode(string(buff), &bookmark)
|
||||
return bookmark, err
|
||||
}
|
||||
|
||||
func fileBasename(path string) string {
|
||||
filename := filepath.Base(path)
|
||||
return strings.Replace(filename, filepath.Ext(path), "", 1)
|
||||
}
|
||||
|
||||
func Path() string {
|
||||
path, _ := homedir.Dir()
|
||||
return fmt.Sprintf("%s/.pgweb/bookmarks", path)
|
||||
}
|
||||
|
||||
func ReadAll(path string) (map[string]Bookmark, error) {
|
||||
results := map[string]Bookmark{}
|
||||
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) != ".toml" {
|
||||
continue
|
||||
}
|
||||
|
||||
fullPath := path + "/" + file.Name()
|
||||
key := fileBasename(file.Name())
|
||||
config, err := readServerConfig(fullPath)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("%s parse error: %s\n", fullPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
results[key] = config
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package bookmarks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Invalid_Bookmark_Files(t *testing.T) {
|
||||
_, err := readServerConfig("foobar")
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = readServerConfig("../../data/invalid.toml")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Near line 1, key 'invalid encoding': Near line 2: Expected key separator '=', but got '\\n' instead.", err.Error())
|
||||
|
||||
_, err = readServerConfig("../../data/invalid_port.toml")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Type mismatch for 'bookmarks.Bookmark.Port': Expected string but found 'int64'.", err.Error())
|
||||
}
|
||||
|
||||
func Test_Bookmark(t *testing.T) {
|
||||
bookmark, err := readServerConfig("../../data/bookmark.toml")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "localhost", bookmark.Host)
|
||||
assert.Equal(t, "5432", bookmark.Port)
|
||||
assert.Equal(t, "postgres", bookmark.User)
|
||||
assert.Equal(t, "mydatabase", bookmark.Database)
|
||||
assert.Equal(t, "disable", bookmark.Ssl)
|
||||
assert.Equal(t, "", bookmark.Password)
|
||||
assert.Equal(t, "", bookmark.Url)
|
||||
}
|
||||
|
||||
func Test_Bookmark_URL(t *testing.T) {
|
||||
bookmark, err := readServerConfig("../../data/bookmark_url.toml")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://username:password@host:port/database?sslmode=disable", bookmark.Url)
|
||||
assert.Equal(t, "", bookmark.Host)
|
||||
assert.Equal(t, "", bookmark.Port)
|
||||
assert.Equal(t, "", bookmark.User)
|
||||
assert.Equal(t, "", bookmark.Database)
|
||||
assert.Equal(t, "", bookmark.Ssl)
|
||||
assert.Equal(t, "", bookmark.Password)
|
||||
}
|
||||
|
||||
func Test_Bookmarks_Path(t *testing.T) {
|
||||
assert.NotEqual(t, "/.pgweb/bookmarks", Path())
|
||||
}
|
||||
|
||||
func Test_Basename(t *testing.T) {
|
||||
assert.Equal(t, "filename", fileBasename("filename.toml"))
|
||||
assert.Equal(t, "filename", fileBasename("path/filename.toml"))
|
||||
assert.Equal(t, "filename", fileBasename("~/long/path/filename.toml"))
|
||||
assert.Equal(t, "filename", fileBasename("filename"))
|
||||
}
|
||||
|
||||
func Test_ReadBookmarks_Invalid(t *testing.T) {
|
||||
bookmarks, err := ReadAll("foobar")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, len(bookmarks))
|
||||
}
|
||||
|
||||
func Test_ReadBookmarks(t *testing.T) {
|
||||
bookmarks, err := ReadAll("../../data")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 2, len(bookmarks))
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
neturl "net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/connection"
|
||||
"github.com/sosedoff/pgweb/pkg/history"
|
||||
"github.com/sosedoff/pgweb/pkg/shared"
|
||||
"github.com/sosedoff/pgweb/pkg/statements"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
db *sqlx.DB
|
||||
tunnel *Tunnel
|
||||
History []history.Record `json:"history"`
|
||||
ConnectionString string `json:"connection_string"`
|
||||
}
|
||||
|
||||
// Struct to hold table rows browsing options
|
||||
type RowsOptions struct {
|
||||
Where string // Custom filter
|
||||
Offset int // Number of rows to skip
|
||||
Limit int // Number of rows to fetch
|
||||
SortColumn string // Column to sort by
|
||||
SortOrder string // Sort direction (ASC, DESC)
|
||||
}
|
||||
|
||||
func getSchemaAndTable(str string) (string, string) {
|
||||
chunks := strings.Split(str, ".")
|
||||
if len(chunks) == 1 {
|
||||
return "public", chunks[0]
|
||||
}
|
||||
return chunks[0], chunks[1]
|
||||
}
|
||||
|
||||
func New() (*Client, error) {
|
||||
str, err := connection.BuildString(command.Opts)
|
||||
|
||||
if command.Opts.Debug && str != "" {
|
||||
fmt.Println("Creating a new client for:", str)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sqlx.Open("postgres", str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := Client{
|
||||
db: db,
|
||||
ConnectionString: str,
|
||||
History: history.New(),
|
||||
}
|
||||
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func NewFromUrl(url string, sshInfo *shared.SSHInfo) (*Client, error) {
|
||||
var tunnel *Tunnel
|
||||
|
||||
if sshInfo != nil {
|
||||
if command.Opts.Debug {
|
||||
fmt.Println("Opening SSH tunnel for:", sshInfo)
|
||||
}
|
||||
|
||||
tunnel, err := NewTunnel(sshInfo, url)
|
||||
if err != nil {
|
||||
tunnel.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tunnel.Configure()
|
||||
if err != nil {
|
||||
tunnel.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go tunnel.Start()
|
||||
|
||||
uri, err := neturl.Parse(url)
|
||||
if err != nil {
|
||||
tunnel.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Override remote postgres port with local proxy port
|
||||
url = strings.Replace(url, uri.Host, fmt.Sprintf("127.0.0.1:%v", tunnel.Port), 1)
|
||||
}
|
||||
|
||||
if command.Opts.Debug {
|
||||
fmt.Println("Creating a new client for:", url)
|
||||
}
|
||||
|
||||
db, err := sqlx.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := Client{
|
||||
db: db,
|
||||
tunnel: tunnel,
|
||||
ConnectionString: url,
|
||||
History: history.New(),
|
||||
}
|
||||
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func (client *Client) Test() error {
|
||||
return client.db.Ping()
|
||||
}
|
||||
|
||||
func (client *Client) Info() (*Result, error) {
|
||||
return client.query(statements.PG_INFO)
|
||||
}
|
||||
|
||||
func (client *Client) Databases() ([]string, error) {
|
||||
return client.fetchRows(statements.PG_DATABASES)
|
||||
}
|
||||
|
||||
func (client *Client) Schemas() ([]string, error) {
|
||||
return client.fetchRows(statements.PG_SCHEMAS)
|
||||
}
|
||||
|
||||
func (client *Client) Objects() (*Result, error) {
|
||||
return client.query(statements.PG_OBJECTS)
|
||||
}
|
||||
|
||||
func (client *Client) Table(table string) (*Result, error) {
|
||||
schema, table := getSchemaAndTable(table)
|
||||
return client.query(statements.PG_TABLE_SCHEMA, schema, table)
|
||||
}
|
||||
|
||||
func (client *Client) MaterializedView(name string) (*Result, error) {
|
||||
return client.query(statements.PG_MATERIALIZED_VIEW_SCHEMA, name)
|
||||
}
|
||||
|
||||
func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error) {
|
||||
schema, table := getSchemaAndTable(table)
|
||||
sql := fmt.Sprintf(`SELECT * FROM "%s"."%s"`, schema, table)
|
||||
|
||||
if opts.Where != "" {
|
||||
sql += fmt.Sprintf(" WHERE %s", opts.Where)
|
||||
}
|
||||
|
||||
if opts.SortColumn != "" {
|
||||
if opts.SortOrder == "" {
|
||||
opts.SortOrder = "ASC"
|
||||
}
|
||||
|
||||
sql += fmt.Sprintf(" ORDER BY %s %s", opts.SortColumn, opts.SortOrder)
|
||||
}
|
||||
|
||||
if opts.Limit > 0 {
|
||||
sql += fmt.Sprintf(" LIMIT %d", opts.Limit)
|
||||
}
|
||||
|
||||
if opts.Offset > 0 {
|
||||
sql += fmt.Sprintf(" OFFSET %d", opts.Offset)
|
||||
}
|
||||
|
||||
return client.query(sql)
|
||||
}
|
||||
|
||||
func (client *Client) TableRowsCount(table string, opts RowsOptions) (*Result, error) {
|
||||
schema, table := getSchemaAndTable(table)
|
||||
sql := fmt.Sprintf(`SELECT COUNT(1) FROM "%s"."%s"`, schema, table)
|
||||
|
||||
if opts.Where != "" {
|
||||
sql += fmt.Sprintf(" WHERE %s", opts.Where)
|
||||
}
|
||||
|
||||
return client.query(sql)
|
||||
}
|
||||
|
||||
func (client *Client) TableInfo(table string) (*Result, error) {
|
||||
return client.query(statements.PG_TABLE_INFO, table)
|
||||
}
|
||||
|
||||
func (client *Client) TableIndexes(table string) (*Result, error) {
|
||||
schema, table := getSchemaAndTable(table)
|
||||
res, err := client.query(statements.PG_TABLE_INDEXES, schema, table)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (client *Client) TableConstraints(table string) (*Result, error) {
|
||||
schema, table := getSchemaAndTable(table)
|
||||
res, err := client.query(statements.PG_TABLE_CONSTRAINTS, schema, table)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Returns all active queriers on the server
|
||||
func (client *Client) Activity() (*Result, error) {
|
||||
return client.query(statements.PG_ACTIVITY)
|
||||
}
|
||||
|
||||
func (client *Client) Query(query string) (*Result, error) {
|
||||
res, err := client.query(query)
|
||||
|
||||
// Save history records only if query did not fail
|
||||
if err == nil && !client.hasHistoryRecord(query) {
|
||||
client.History = append(client.History, history.NewRecord(query))
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (client *Client) query(query string, args ...interface{}) (*Result, error) {
|
||||
action := strings.ToLower(strings.Split(query, " ")[0])
|
||||
if action == "update" || action == "delete" {
|
||||
res, err := client.db.Exec(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := Result{
|
||||
Columns: []string{"Rows Affected"},
|
||||
Rows: []Row{
|
||||
Row{affected},
|
||||
},
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
rows, err := client.db.Queryx(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure to never return null colums
|
||||
if cols == nil {
|
||||
cols = []string{}
|
||||
}
|
||||
|
||||
result := Result{
|
||||
Columns: cols,
|
||||
Rows: []Row{},
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
obj, err := rows.SliceScan()
|
||||
|
||||
for i, item := range obj {
|
||||
if item == nil {
|
||||
obj[i] = nil
|
||||
} else {
|
||||
t := reflect.TypeOf(item).Kind().String()
|
||||
|
||||
if t == "slice" {
|
||||
obj[i] = string(item.([]byte))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
result.Rows = append(result.Rows, obj)
|
||||
}
|
||||
}
|
||||
|
||||
result.PrepareBigints()
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
func (client *Client) Close() error {
|
||||
if client.tunnel != nil {
|
||||
client.tunnel.Close()
|
||||
}
|
||||
|
||||
if client.db != nil {
|
||||
return client.db.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch all rows as strings for a single column
|
||||
func (client *Client) fetchRows(q string) ([]string, error) {
|
||||
res, err := client.query(q)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Init empty slice so json.Marshal will encode it to "[]" instead of "null"
|
||||
results := make([]string, 0)
|
||||
|
||||
for _, row := range res.Rows {
|
||||
results = append(results, row[0].(string))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (client *Client) hasHistoryRecord(query string) bool {
|
||||
result := false
|
||||
|
||||
for _, record := range client.History {
|
||||
if record.Query == query {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
testClient *Client
|
||||
testCommands map[string]string
|
||||
serverHost string
|
||||
serverPort string
|
||||
serverUser string
|
||||
serverPassword string
|
||||
serverDatabase string
|
||||
)
|
||||
|
||||
func mapKeys(data map[string]*Objects) []string {
|
||||
result := []string{}
|
||||
for k, _ := range data {
|
||||
result = append(result, k)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func pgVersion() (int, int) {
|
||||
var major, minor int
|
||||
fmt.Sscanf(os.Getenv("PGVERSION"), "%d.%d", &major, &minor)
|
||||
return major, minor
|
||||
}
|
||||
|
||||
func getVar(name, def string) string {
|
||||
val := os.Getenv(name)
|
||||
if val == "" {
|
||||
return def
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func initVars() {
|
||||
serverHost = getVar("PGHOST", "localhost")
|
||||
serverPort = getVar("PGPORT", "5432")
|
||||
serverUser = getVar("PGUSER", "postgres")
|
||||
serverPassword = getVar("PGPASSWORD", "postgres")
|
||||
serverDatabase = getVar("PGDATABASE", "booktown")
|
||||
}
|
||||
|
||||
func setupCommands() {
|
||||
testCommands = map[string]string{
|
||||
"createdb": "createdb",
|
||||
"psql": "psql",
|
||||
"dropdb": "dropdb",
|
||||
}
|
||||
|
||||
if onWindows() {
|
||||
for k, v := range testCommands {
|
||||
testCommands[k] = v + ".exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onWindows() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
}
|
||||
|
||||
func setup() {
|
||||
out, err := exec.Command(
|
||||
testCommands["createdb"],
|
||||
"-U", serverUser,
|
||||
"-h", serverHost,
|
||||
"-p", serverPort,
|
||||
serverDatabase,
|
||||
).CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Database creation failed:", string(out))
|
||||
fmt.Println("Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
out, err = exec.Command(
|
||||
testCommands["psql"],
|
||||
"-U", serverUser,
|
||||
"-h", serverHost,
|
||||
"-p", serverPort,
|
||||
"-f", "../../data/booktown.sql",
|
||||
serverDatabase,
|
||||
).CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Database import failed:", string(out))
|
||||
fmt.Println("Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func setupClient() {
|
||||
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||
testClient, _ = NewFromUrl(url, nil)
|
||||
}
|
||||
|
||||
func teardownClient() {
|
||||
if testClient != nil {
|
||||
testClient.db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func teardown() {
|
||||
_, err := exec.Command(
|
||||
testCommands["dropdb"],
|
||||
"-U", serverUser,
|
||||
"-h", serverHost,
|
||||
"-p", serverPort,
|
||||
serverDatabase,
|
||||
).CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Teardown error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func test_NewClientFromUrl(t *testing.T) {
|
||||
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||
client, err := NewFromUrl(url, nil)
|
||||
|
||||
if err != nil {
|
||||
defer client.Close()
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, url, client.ConnectionString)
|
||||
}
|
||||
|
||||
func test_NewClientFromUrl2(t *testing.T) {
|
||||
url := fmt.Sprintf("postgresql://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||
client, err := NewFromUrl(url, nil)
|
||||
|
||||
if err != nil {
|
||||
defer client.Close()
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, url, client.ConnectionString)
|
||||
}
|
||||
|
||||
func test_Test(t *testing.T) {
|
||||
assert.Equal(t, nil, testClient.Test())
|
||||
}
|
||||
|
||||
func test_Info(t *testing.T) {
|
||||
res, err := testClient.Info()
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NotEqual(t, nil, res)
|
||||
}
|
||||
|
||||
func test_Databases(t *testing.T) {
|
||||
res, err := testClient.Databases()
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Contains(t, res, "booktown")
|
||||
assert.Contains(t, res, "postgres")
|
||||
}
|
||||
|
||||
func test_Objects(t *testing.T) {
|
||||
res, err := testClient.Objects()
|
||||
objects := ObjectsFromResult(res)
|
||||
|
||||
tables := []string{
|
||||
"alternate_stock",
|
||||
"authors",
|
||||
"book_backup",
|
||||
"book_queue",
|
||||
"books",
|
||||
"customers",
|
||||
"daily_inventory",
|
||||
"distinguished_authors",
|
||||
"editions",
|
||||
"employees",
|
||||
"favorite_authors",
|
||||
"favorite_books",
|
||||
"money_example",
|
||||
"my_list",
|
||||
"numeric_values",
|
||||
"publishers",
|
||||
"schedules",
|
||||
"shipments",
|
||||
"states",
|
||||
"stock",
|
||||
"stock_backup",
|
||||
"subjects",
|
||||
"text_sorting",
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, []string{"schema", "name", "type", "owner"}, res.Columns)
|
||||
assert.Equal(t, []string{"public"}, mapKeys(objects))
|
||||
assert.Equal(t, tables, objects["public"].Tables)
|
||||
assert.Equal(t, []string{"recent_shipments", "stock_view"}, objects["public"].Views)
|
||||
assert.Equal(t, []string{"author_ids", "book_ids", "shipments_ship_id_seq", "subject_ids"}, objects["public"].Sequences)
|
||||
|
||||
major, minor := pgVersion()
|
||||
if minor == 0 || minor >= 3 {
|
||||
assert.Equal(t, []string{"m_stock_view"}, objects["public"].MaterializedViews)
|
||||
} else {
|
||||
t.Logf("Skipping materialized view on %d.%d\n", major, minor)
|
||||
}
|
||||
}
|
||||
|
||||
func test_Table(t *testing.T) {
|
||||
res, err := testClient.Table("books")
|
||||
|
||||
columns := []string{
|
||||
"column_name",
|
||||
"data_type",
|
||||
"is_nullable",
|
||||
"character_maximum_length",
|
||||
"character_set_catalog",
|
||||
"column_default",
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, columns, res.Columns)
|
||||
assert.Equal(t, 4, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableRows(t *testing.T) {
|
||||
res, err := testClient.TableRows("books", RowsOptions{})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 4, len(res.Columns))
|
||||
assert.Equal(t, 15, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableInfo(t *testing.T) {
|
||||
res, err := testClient.TableInfo("books")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 4, len(res.Columns))
|
||||
assert.Equal(t, 1, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableIndexes(t *testing.T) {
|
||||
res, err := testClient.TableIndexes("books")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 2, len(res.Columns))
|
||||
assert.Equal(t, 2, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableConstraints(t *testing.T) {
|
||||
res, err := testClient.TableConstraints("editions")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 1, len(res.Columns))
|
||||
assert.Equal(t, 2, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_Query(t *testing.T) {
|
||||
res, err := testClient.Query("SELECT * FROM books")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 4, len(res.Columns))
|
||||
assert.Equal(t, 15, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_QueryError(t *testing.T) {
|
||||
res, err := testClient.Query("SELCT * FROM books")
|
||||
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, "pq: syntax error at or near \"SELCT\"", err.Error())
|
||||
assert.Equal(t, true, res == nil)
|
||||
}
|
||||
|
||||
func test_QueryInvalidTable(t *testing.T) {
|
||||
res, err := testClient.Query("SELECT * FROM books2")
|
||||
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, "pq: relation \"books2\" does not exist", err.Error())
|
||||
assert.Equal(t, true, res == nil)
|
||||
}
|
||||
|
||||
func test_ResultCsv(t *testing.T) {
|
||||
res, _ := testClient.Query("SELECT * FROM books ORDER BY id ASC LIMIT 1")
|
||||
csv := res.CSV()
|
||||
|
||||
expected := "id,title,author_id,subject_id\n156,The Tell-Tale Heart,115,9\n"
|
||||
|
||||
assert.Equal(t, expected, string(csv))
|
||||
}
|
||||
|
||||
func test_History(t *testing.T) {
|
||||
_, err := testClient.Query("SELECT * FROM books WHERE id = 12345")
|
||||
query := testClient.History[len(testClient.History)-1].Query
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "SELECT * FROM books WHERE id = 12345", query)
|
||||
}
|
||||
|
||||
func test_HistoryError(t *testing.T) {
|
||||
_, err := testClient.Query("SELECT * FROM books123")
|
||||
query := testClient.History[len(testClient.History)-1].Query
|
||||
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.NotEqual(t, "SELECT * FROM books123", query)
|
||||
}
|
||||
|
||||
func test_HistoryUniqueness(t *testing.T) {
|
||||
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||
client, _ := NewFromUrl(url, nil)
|
||||
|
||||
client.Query("SELECT * FROM books WHERE id = 1")
|
||||
client.Query("SELECT * FROM books WHERE id = 1")
|
||||
|
||||
assert.Equal(t, 1, len(client.History))
|
||||
assert.Equal(t, "SELECT * FROM books WHERE id = 1", client.History[0].Query)
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
if onWindows() {
|
||||
// Dont have access to windows machines at the moment...
|
||||
return
|
||||
}
|
||||
|
||||
initVars()
|
||||
setupCommands()
|
||||
teardown()
|
||||
setup()
|
||||
setupClient()
|
||||
|
||||
test_NewClientFromUrl(t)
|
||||
test_Test(t)
|
||||
test_Info(t)
|
||||
test_Databases(t)
|
||||
test_Objects(t)
|
||||
test_Table(t)
|
||||
test_TableRows(t)
|
||||
test_TableInfo(t)
|
||||
test_TableIndexes(t)
|
||||
test_TableConstraints(t)
|
||||
test_Query(t)
|
||||
test_QueryError(t)
|
||||
test_QueryInvalidTable(t)
|
||||
test_ResultCsv(t)
|
||||
test_History(t)
|
||||
test_HistoryError(t)
|
||||
|
||||
teardownClient()
|
||||
teardown()
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Row []interface{}
|
||||
|
||||
type Pagination struct {
|
||||
Rows int64 `json:"rows_count"`
|
||||
Page int64 `json:"page"`
|
||||
Pages int64 `json:"pages_count"`
|
||||
PerPage int64 `json:"per_page"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Pagination *Pagination `json:"pagination,omitempty"`
|
||||
Columns []string `json:"columns"`
|
||||
Rows []Row `json:"rows"`
|
||||
}
|
||||
|
||||
type Objects struct {
|
||||
Tables []string `json:"table"`
|
||||
Views []string `json:"view"`
|
||||
MaterializedViews []string `json:"materialized_view"`
|
||||
Sequences []string `json:"sequence"`
|
||||
}
|
||||
|
||||
// Due to big int number limitations in javascript, numbers should be encoded
|
||||
// as strings so they could be properly loaded on the frontend.
|
||||
func (res *Result) PrepareBigints() {
|
||||
for i, row := range res.Rows {
|
||||
for j, col := range row {
|
||||
if col == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch reflect.TypeOf(col).Kind() {
|
||||
case reflect.Int64:
|
||||
val := col.(int64)
|
||||
if val < -9007199254740991 || val > 9007199254740991 {
|
||||
res.Rows[i][j] = strconv.FormatInt(col.(int64), 10)
|
||||
}
|
||||
case reflect.Float64:
|
||||
val := col.(float64)
|
||||
if val < -999999999999999 || val > 999999999999999 {
|
||||
res.Rows[i][j] = strconv.FormatFloat(val, 'e', -1, 64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (res *Result) Format() []map[string]interface{} {
|
||||
var items []map[string]interface{}
|
||||
|
||||
for _, row := range res.Rows {
|
||||
item := make(map[string]interface{})
|
||||
|
||||
for i, c := range res.Columns {
|
||||
item[c] = row[i]
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (res *Result) CSV() []byte {
|
||||
buff := &bytes.Buffer{}
|
||||
writer := csv.NewWriter(buff)
|
||||
|
||||
writer.Write(res.Columns)
|
||||
|
||||
for _, row := range res.Rows {
|
||||
record := make([]string, len(res.Columns))
|
||||
|
||||
for i, item := range row {
|
||||
if item != nil {
|
||||
switch v := item.(type) {
|
||||
case time.Time:
|
||||
record[i] = v.Format("2006-01-02 15:04:05")
|
||||
default:
|
||||
record[i] = fmt.Sprintf("%v", item)
|
||||
}
|
||||
} else {
|
||||
record[i] = ""
|
||||
}
|
||||
}
|
||||
|
||||
err := writer.Write(record)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
writer.Flush()
|
||||
return buff.Bytes()
|
||||
}
|
||||
|
||||
func (res *Result) JSON() []byte {
|
||||
data, _ := json.Marshal(res.Format())
|
||||
return data
|
||||
}
|
||||
|
||||
func ObjectsFromResult(res *Result) map[string]*Objects {
|
||||
objects := map[string]*Objects{}
|
||||
|
||||
for _, row := range res.Rows {
|
||||
schema := row[0].(string)
|
||||
name := row[1].(string)
|
||||
object_type := row[2].(string)
|
||||
|
||||
if objects[schema] == nil {
|
||||
objects[schema] = &Objects{
|
||||
Tables: []string{},
|
||||
Views: []string{},
|
||||
MaterializedViews: []string{},
|
||||
Sequences: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
switch object_type {
|
||||
case "table":
|
||||
objects[schema].Tables = append(objects[schema].Tables, name)
|
||||
case "view":
|
||||
objects[schema].Views = append(objects[schema].Views, name)
|
||||
case "materialized_view":
|
||||
objects[schema].MaterializedViews = append(objects[schema].MaterializedViews, name)
|
||||
case "sequence":
|
||||
objects[schema].Sequences = append(objects[schema].Sequences, name)
|
||||
}
|
||||
}
|
||||
|
||||
return objects
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_PrepareBigints(t *testing.T) {
|
||||
result := Result{
|
||||
Columns: []string{"value"},
|
||||
Rows: []Row{
|
||||
Row{int(1234)},
|
||||
Row{int64(9223372036854775807)},
|
||||
Row{int64(-9223372036854775808)},
|
||||
Row{float64(9223372036854775808.9223372036854775808)},
|
||||
Row{float64(999999999999999.9)},
|
||||
},
|
||||
}
|
||||
|
||||
result.PrepareBigints()
|
||||
|
||||
assert.Equal(t, 1234, result.Rows[0][0])
|
||||
assert.Equal(t, "9223372036854775807", result.Rows[1][0])
|
||||
assert.Equal(t, "-9223372036854775808", result.Rows[2][0])
|
||||
assert.Equal(t, "9.223372036854776e+18", result.Rows[3][0])
|
||||
assert.Equal(t, "9.999999999999999e+14", result.Rows[4][0])
|
||||
}
|
||||
|
||||
func Test_CSV(t *testing.T) {
|
||||
result := Result{
|
||||
Columns: []string{"id", "name", "email"},
|
||||
Rows: []Row{
|
||||
Row{1, "John", "john@example.com"},
|
||||
Row{2, "Bob", "bob@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
expected := "id,name,email\n1,John,john@example.com\n2,Bob,bob@example.com\n"
|
||||
output := string(result.CSV())
|
||||
|
||||
assert.Equal(t, expected, output)
|
||||
}
|
||||
|
||||
func Test_JSON(t *testing.T) {
|
||||
result := Result{
|
||||
Columns: []string{"id", "name", "email"},
|
||||
Rows: []Row{
|
||||
Row{1, "John", "john@example.com"},
|
||||
Row{2, "Bob", "bob@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
obj := []struct {
|
||||
Id int
|
||||
Name string
|
||||
Email string
|
||||
}{}
|
||||
|
||||
expected := []struct {
|
||||
Id int
|
||||
Name string
|
||||
Email string
|
||||
}{
|
||||
{1, "John", "john@example.com"},
|
||||
{2, "Bob", "bob@example.com"},
|
||||
}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(result.JSON(), &obj))
|
||||
assert.Equal(t, 2, len(obj))
|
||||
assert.Equal(t, expected, obj)
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/connection"
|
||||
"github.com/sosedoff/pgweb/pkg/shared"
|
||||
)
|
||||
|
||||
const (
|
||||
PORT_START = 29168
|
||||
PORT_LIMIT = 500
|
||||
)
|
||||
|
||||
type Tunnel struct {
|
||||
TargetHost string
|
||||
TargetPort string
|
||||
Port int
|
||||
SSHInfo *shared.SSHInfo
|
||||
Config *ssh.ClientConfig
|
||||
Client *ssh.Client
|
||||
Listener *net.TCPListener
|
||||
}
|
||||
|
||||
func privateKeyPath() string {
|
||||
return os.Getenv("HOME") + "/.ssh/id_rsa"
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func parsePrivateKey(keyPath string) (ssh.Signer, error) {
|
||||
buff, err := ioutil.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ssh.ParsePrivateKey(buff)
|
||||
}
|
||||
|
||||
func makeConfig(info *shared.SSHInfo) (*ssh.ClientConfig, error) {
|
||||
methods := []ssh.AuthMethod{}
|
||||
|
||||
keyPath := privateKeyPath()
|
||||
if fileExists(keyPath) {
|
||||
key, err := parsePrivateKey(keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
methods = append(methods, ssh.PublicKeys(key))
|
||||
}
|
||||
|
||||
methods = append(methods, ssh.Password(info.Password))
|
||||
|
||||
return &ssh.ClientConfig{User: info.User, Auth: methods}, nil
|
||||
}
|
||||
|
||||
func (tunnel *Tunnel) sshEndpoint() string {
|
||||
return fmt.Sprintf("%s:%v", tunnel.SSHInfo.Host, tunnel.SSHInfo.Port)
|
||||
}
|
||||
|
||||
func (tunnel *Tunnel) targetEndpoint() string {
|
||||
return fmt.Sprintf("%v:%v", tunnel.TargetHost, tunnel.TargetPort)
|
||||
}
|
||||
|
||||
func (tunnel *Tunnel) copy(wg *sync.WaitGroup, writer, reader net.Conn) {
|
||||
defer wg.Done()
|
||||
if _, err := io.Copy(writer, reader); err != nil {
|
||||
log.Println("Tunnel copy error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (tunnel *Tunnel) handleConnection(local net.Conn) {
|
||||
remote, err := tunnel.Client.Dial("tcp", tunnel.targetEndpoint())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
go tunnel.copy(&wg, local, remote)
|
||||
go tunnel.copy(&wg, remote, local)
|
||||
|
||||
wg.Wait()
|
||||
local.Close()
|
||||
}
|
||||
|
||||
func (tunnel *Tunnel) Close() {
|
||||
if tunnel.Client != nil {
|
||||
tunnel.Client.Close()
|
||||
}
|
||||
|
||||
if tunnel.Listener != nil {
|
||||
tunnel.Listener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (tunnel *Tunnel) Configure() error {
|
||||
config, err := makeConfig(tunnel.SSHInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tunnel.Config = config
|
||||
|
||||
client, err := ssh.Dial("tcp", tunnel.sshEndpoint(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tunnel.Client = client
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%v", tunnel.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tunnel.Listener = listener.(*net.TCPListener)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tunnel *Tunnel) Start() {
|
||||
defer tunnel.Close()
|
||||
|
||||
for {
|
||||
conn, err := tunnel.Listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go tunnel.handleConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func NewTunnel(sshInfo *shared.SSHInfo, dbUrl string) (*Tunnel, error) {
|
||||
uri, err := url.Parse(dbUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listenPort, err := connection.AvailablePort(PORT_START, PORT_LIMIT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chunks := strings.Split(uri.Host, ":")
|
||||
host := chunks[0]
|
||||
port := "5432"
|
||||
|
||||
if len(chunks) == 2 {
|
||||
port = chunks[1]
|
||||
}
|
||||
|
||||
tunnel := &Tunnel{
|
||||
Port: listenPort,
|
||||
SSHInfo: sshInfo,
|
||||
TargetHost: host,
|
||||
TargetPort: port,
|
||||
}
|
||||
|
||||
return tunnel, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Version bool `short:"v" long:"version" description:"Print version"`
|
||||
Debug bool `short:"d" long:"debug" description:"Enable debugging mode" default:"false"`
|
||||
Url string `long:"url" description:"Database connection string"`
|
||||
Host string `long:"host" description:"Server hostname or IP"`
|
||||
Port int `long:"port" description:"Server port" default:"5432"`
|
||||
User string `long:"user" description:"Database user"`
|
||||
Pass string `long:"pass" description:"Password for user"`
|
||||
DbName string `long:"db" description:"Database name"`
|
||||
Ssl string `long:"ssl" description:"SSL option"`
|
||||
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
|
||||
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8081"`
|
||||
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
|
||||
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
|
||||
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
|
||||
Sessions bool `long:"sessions" description:"Enable multiple database sessions" default:"false"`
|
||||
Prefix string `long:"prefix" description:"Add a url prefix"`
|
||||
}
|
||||
|
||||
var Opts Options
|
||||
|
||||
func ParseOptions() error {
|
||||
_, err := flags.ParseArgs(&Opts, os.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if Opts.Url == "" {
|
||||
Opts.Url = os.Getenv("DATABASE_URL")
|
||||
}
|
||||
|
||||
if os.Getenv("SESSIONS") != "" {
|
||||
Opts.Sessions = true
|
||||
}
|
||||
|
||||
if Opts.Prefix != "" && !strings.Contains(Opts.Prefix, "/") {
|
||||
Opts.Prefix = Opts.Prefix + "/"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Options(t *testing.T) {
|
||||
err := ParseOptions()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, Opts.Sessions)
|
||||
assert.Equal(t, "", Opts.Prefix)
|
||||
}
|
||||
|
||||
func Test_SessionsOption(t *testing.T) {
|
||||
oldargs := os.Args
|
||||
defer func() { os.Args = oldargs }()
|
||||
|
||||
os.Args = []string{"--sessions", "1"}
|
||||
assert.NoError(t, ParseOptions())
|
||||
assert.Equal(t, true, Opts.Sessions)
|
||||
}
|
||||
|
||||
func Test_PrefixOption(t *testing.T) {
|
||||
oldargs := os.Args
|
||||
defer func() { os.Args = oldargs }()
|
||||
|
||||
os.Args = []string{"--prefix", "pgweb"}
|
||||
assert.NoError(t, ParseOptions())
|
||||
assert.Equal(t, "pgweb/", Opts.Prefix)
|
||||
|
||||
os.Args = []string{"--prefix", "pgweb/"}
|
||||
assert.NoError(t, ParseOptions())
|
||||
assert.Equal(t, "pgweb/", Opts.Prefix)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package command
|
||||
|
||||
const VERSION = "0.9.4"
|
||||
|
||||
var (
|
||||
GitCommit string
|
||||
BuildTime string
|
||||
)
|
||||
@@ -0,0 +1,101 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
neturl "net/url"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
)
|
||||
|
||||
func currentUser() (string, error) {
|
||||
u, err := user.Current()
|
||||
if err == nil {
|
||||
return u.Username, nil
|
||||
}
|
||||
|
||||
name := os.Getenv("USER")
|
||||
if name != "" {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
return "", errors.New("Unable to detect OS user")
|
||||
}
|
||||
|
||||
func FormatUrl(opts command.Options) (string, error) {
|
||||
url := opts.Url
|
||||
|
||||
// Make sure to only accept urls in a standard format
|
||||
if !strings.HasPrefix(url, "postgres://") && !strings.HasPrefix(url, "postgresql://") {
|
||||
return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
|
||||
}
|
||||
|
||||
// Special handling for local connections
|
||||
if strings.Contains(url, "localhost") || strings.Contains(url, "127.0.0.1") {
|
||||
if !strings.Contains(url, "?sslmode") {
|
||||
if opts.Ssl == "" {
|
||||
url += fmt.Sprintf("?sslmode=%s", "disable")
|
||||
} else {
|
||||
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append sslmode parameter only if its defined as a flag and not present
|
||||
// in the connection string.
|
||||
if !strings.Contains(url, "?sslmode") && opts.Ssl != "" {
|
||||
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func IsBlank(opts command.Options) bool {
|
||||
return opts.Host == "" && opts.User == "" && opts.DbName == "" && opts.Url == ""
|
||||
}
|
||||
|
||||
func BuildString(opts command.Options) (string, error) {
|
||||
if opts.Url != "" {
|
||||
return FormatUrl(opts)
|
||||
}
|
||||
|
||||
// Try to detect user from current OS user
|
||||
if opts.User == "" {
|
||||
u, err := currentUser()
|
||||
if err == nil {
|
||||
opts.User = u
|
||||
}
|
||||
}
|
||||
|
||||
// Disable ssl for localhost connections, most users have it disabled
|
||||
if opts.Host == "localhost" || opts.Host == "127.0.0.1" {
|
||||
if opts.Ssl == "" {
|
||||
opts.Ssl = "disable"
|
||||
}
|
||||
}
|
||||
|
||||
url := "postgres://"
|
||||
|
||||
if opts.User != "" {
|
||||
url += opts.User
|
||||
}
|
||||
|
||||
if opts.Pass != "" {
|
||||
url += fmt.Sprintf(":%s", neturl.QueryEscape(opts.Pass))
|
||||
}
|
||||
|
||||
url += fmt.Sprintf("@%s:%d", opts.Host, opts.Port)
|
||||
|
||||
if opts.DbName != "" {
|
||||
url += fmt.Sprintf("/%s", opts.DbName)
|
||||
}
|
||||
|
||||
if opts.Ssl != "" {
|
||||
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/user"
|
||||
"testing"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Invalid_Url(t *testing.T) {
|
||||
opts := command.Options{}
|
||||
examples := []string{
|
||||
"postgre://foobar",
|
||||
"foobar",
|
||||
}
|
||||
|
||||
for _, val := range examples {
|
||||
opts.Url = val
|
||||
str, err := BuildString(opts)
|
||||
|
||||
assert.Equal(t, "", str)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Valid_Url(t *testing.T) {
|
||||
url := "postgres://myhost/database"
|
||||
str, err := BuildString(command.Options{Url: url})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, url, str)
|
||||
}
|
||||
|
||||
func Test_Url_And_Ssl_Flag(t *testing.T) {
|
||||
str, err := BuildString(command.Options{
|
||||
Url: "postgres://myhost/database",
|
||||
Ssl: "disable",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://myhost/database?sslmode=disable", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_Url_And_No_Ssl_Flag(t *testing.T) {
|
||||
str, err := BuildString(command.Options{
|
||||
Url: "postgres://localhost/database",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=disable", str)
|
||||
|
||||
str, err = BuildString(command.Options{
|
||||
Url: "postgres://127.0.0.1/database",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=disable", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
|
||||
str, err := BuildString(command.Options{
|
||||
Url: "postgres://localhost/database",
|
||||
Ssl: "require",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
||||
|
||||
str, err = BuildString(command.Options{
|
||||
Url: "postgres://127.0.0.1/database",
|
||||
Ssl: "require",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_Url_And_Ssl_Arg(t *testing.T) {
|
||||
str, err := BuildString(command.Options{
|
||||
Url: "postgres://localhost/database?sslmode=require",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
||||
|
||||
str, err = BuildString(command.Options{
|
||||
Url: "postgres://127.0.0.1/database?sslmode=require",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
||||
}
|
||||
|
||||
func Test_Flag_Args(t *testing.T) {
|
||||
str, err := BuildString(command.Options{
|
||||
Host: "host",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@host:5432/db", str)
|
||||
}
|
||||
|
||||
func Test_Localhost(t *testing.T) {
|
||||
opts := command.Options{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
}
|
||||
|
||||
str, err := BuildString(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str)
|
||||
|
||||
opts.Host = "127.0.0.1"
|
||||
str, err = BuildString(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_And_Ssl(t *testing.T) {
|
||||
opts := command.Options{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
Ssl: "require",
|
||||
}
|
||||
|
||||
str, err := BuildString(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=require", str)
|
||||
}
|
||||
|
||||
func Test_No_User(t *testing.T) {
|
||||
opts := command.Options{Host: "host", Port: 5432, DbName: "db"}
|
||||
u, _ := user.Current()
|
||||
str, err := BuildString(opts)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", u.Username), str)
|
||||
}
|
||||
|
||||
func Test_Port(t *testing.T) {
|
||||
opts := command.Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
|
||||
str, err := BuildString(opts)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user@host:5000/db", str)
|
||||
}
|
||||
|
||||
func Test_Blank(t *testing.T) {
|
||||
assert.Equal(t, true, IsBlank(command.Options{}))
|
||||
assert.Equal(t, false, IsBlank(command.Options{Host: "host", User: "user"}))
|
||||
assert.Equal(t, false, IsBlank(command.Options{Host: "host", User: "user", DbName: "db"}))
|
||||
assert.Equal(t, false, IsBlank(command.Options{Url: "url"}))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user