feature: add ngx lua github action and test cases. (#7)

* add test cases for ngx lua.

* add more test cases.

* add ngx lua to github action.

* Compatible with lua and ngx lua

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 05972cf..f735fe3 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -19,11 +19,10 @@
 on:
   pull_request:
   push:
-    branches: 
+    branches:
       - master
     tags:
       - 'v*'
-    
 
 jobs:
   CI:
@@ -49,10 +48,28 @@
         run: |
           sudo luarocks install luaunit
           sudo luarocks install lua-cjson 2.1.0-1
-      - name: 'Run Tests'
+      - name: "Install OpenResty"
+        run: |
+          wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
+          sudo apt-get -y update --fix-missing
+          sudo apt-get -y install software-properties-common
+          sudo add-apt-repository -y "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main"
+          sudo apt-get update
+          sudo apt-get install openresty-debug
+      - name: "Install test::nginx for testing"
+        run: |
+          sudo apt-get install -y cpanminus
+          sudo cpanm --notest Test::Nginx >build.log 2>&1 || (cat build.log && exit 1)
+          git clone https://github.com/iresty/test-nginx.git test-nginx
+      - name: 'Run Lua Tests'
         run: |
           cd lib/skywalking
           lua util_test.lua
-          lua span_test.lua 
-          lua tracing_context_test.lua 
-          lua segment_ref_test.lua 
\ No newline at end of file
+          lua span_test.lua
+          lua tracing_context_test.lua
+          lua segment_ref_test.lua
+          cd ..
+      - name: 'Run Nginx Lua Tests'
+        run: |
+          export PATH=/usr/local/openresty-debug/nginx/sbin:/usr/local/openresty-debug/luajit/bin:$/usr/local/openresty-debug/bin:$PATH
+          prove -Itest-nginx/lib -r t
diff --git a/lib/skywalking/segment.lua b/lib/skywalking/segment.lua
index 346842d..ea2f33e 100644
--- a/lib/skywalking/segment.lua
+++ b/lib/skywalking/segment.lua
@@ -1,19 +1,19 @@
--- 
+--
 -- Licensed to the Apache Software Foundation (ASF) under one or more
 -- contributor license agreements.  See the NOTICE file distributed with
 -- this work for additional information regarding copyright ownership.
 -- The ASF licenses this file to You under the Apache License, Version 2.0
 -- (the "License"); you may not use this file except in compliance with
 -- the License.  You may obtain a copy of the License at
--- 
+--
 --    http://www.apache.org/licenses/LICENSE-2.0
--- 
+--
 -- Unless required by applicable law or agreed to in writing, software
 -- distributed under the License is distributed on an "AS IS" BASIS,
 -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
--- 
+--
 
 -- Segment represents a finished tracing context
 -- Including all information to send to the SkyWalking OAP server.
@@ -78,4 +78,4 @@
     return segmentBuilder
 end
 
-return Segment
\ No newline at end of file
+return Segment
diff --git a/lib/skywalking/segment_ref.lua b/lib/skywalking/segment_ref.lua
index b7c7f67..7709e90 100644
--- a/lib/skywalking/segment_ref.lua
+++ b/lib/skywalking/segment_ref.lua
@@ -1,20 +1,19 @@
--- 
+--
 -- Licensed to the Apache Software Foundation (ASF) under one or more
 -- contributor license agreements.  See the NOTICE file distributed with
 -- this work for additional information regarding copyright ownership.
 -- The ASF licenses this file to You under the Apache License, Version 2.0
 -- (the "License"); you may not use this file except in compliance with
 -- the License.  You may obtain a copy of the License at
--- 
+--
 --    http://www.apache.org/licenses/LICENSE-2.0
--- 
+--
 -- Unless required by applicable law or agreed to in writing, software
 -- distributed under the License is distributed on an "AS IS" BASIS,
 -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
--- 
-
+--
 local Util = require('util')
 local Base64 = require('dependencies/base64')
 
@@ -71,13 +70,13 @@
 
 -- Deserialize value from the propagated context and initialize the SegmentRef
 function SegmentRef:fromSW6Value(value)
-    local parts = Util: split(value, '-')
+    local parts = Util.split(value, '-')
     if #parts ~= 9 then
         return nil
     end
 
-    self.trace_id = Util:formatID(Base64.decode(parts[2]))
-    self.segment_id = Util:formatID(Base64.decode(parts[3]))
+    self.trace_id = Util.formatID(Base64.decode(parts[2]))
+    self.segment_id = Util.formatID(Base64.decode(parts[3]))
     self.span_id = tonumber(parts[4])
     self.parent_service_instance_id = tonumber(parts[5])
     self.entry_service_instance_id = tonumber(parts[6])
@@ -106,13 +105,13 @@
 -- Return string to represent this ref.
 function SegmentRef:serialize()
     local encodedRef = '1'
-    encodedRef = encodedRef .. '-' .. Base64.encode(Util:id2String(self.trace_id))
-    encodedRef = encodedRef .. '-' .. Base64.encode(Util:id2String(self.segment_id))
+    encodedRef = encodedRef .. '-' .. Base64.encode(Util.id2String(self.trace_id))
+    encodedRef = encodedRef .. '-' .. Base64.encode(Util.id2String(self.segment_id))
     encodedRef = encodedRef .. '-' .. self.span_id
     encodedRef = encodedRef .. '-' .. self.parent_service_instance_id
     encodedRef = encodedRef .. '-' .. self.entry_service_instance_id
 
-    local networkAddress 
+    local networkAddress
     if self.network_address_id ~= 0 then
         networkAddress = self.network_address_id .. ''
     else
diff --git a/lib/skywalking/tracing_context.lua b/lib/skywalking/tracing_context.lua
index 47fcfe6..427202e 100644
--- a/lib/skywalking/tracing_context.lua
+++ b/lib/skywalking/tracing_context.lua
@@ -1,19 +1,19 @@
--- 
+--
 -- Licensed to the Apache Software Foundation (ASF) under one or more
 -- contributor license agreements.  See the NOTICE file distributed with
 -- this work for additional information regarding copyright ownership.
 -- The ASF licenses this file to You under the Apache License, Version 2.0
 -- (the "License"); you may not use this file except in compliance with
 -- the License.  You may obtain a copy of the License at
--- 
+--
 --    http://www.apache.org/licenses/LICENSE-2.0
--- 
+--
 -- Unless required by applicable law or agreed to in writing, software
 -- distributed under the License is distributed on an "AS IS" BASIS,
 -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
--- 
+--
 
 local Util = require('util')
 local Span = require('span')
@@ -58,7 +58,7 @@
         return TracingContext:newNoOP()
     end
 
-    o.trace_id = Util:newID()
+    o.trace_id = Util.newID()
     o.segment_id = o.trace_id
     o.service_id = serviceId
     o.service_inst_id = serviceInstID
@@ -99,8 +99,8 @@
 -- After all active spans finished, this segment will be treated as finished status.
 -- Notice, it is different with Java agent, a finished context is still able to recreate new span, and be checked as finished again.
 -- This gives the end user more flexibility. Unless it is a real reasonable case, don't call #drainAfterFinished multiple times.
--- 
--- Return (boolean isSegmentFinished, Segment segment). 
+--
+-- Return (boolean isSegmentFinished, Segment segment).
 -- Segment has value only when the isSegmentFinished is true
 -- if isSegmentFinished == false, SpanList = nil
 function TracingContext:drainAfterFinished()
@@ -163,7 +163,7 @@
         self.first_span = span
     end
 
-    -- span id starts at 0, to fit LUA, we need to plus one.    
+    -- span id starts at 0, to fit LUA, we need to plus one.
     self.active_spans[span.span_id + 1] = span
     self.active_count = self.active_count + 1
     return self.owner
@@ -186,4 +186,4 @@
 end
 ---------------------------------------------
 
-return TracingContext
\ No newline at end of file
+return TracingContext
diff --git a/lib/skywalking/util.lua b/lib/skywalking/util.lua
index 27c1886..8a349fa 100644
--- a/lib/skywalking/util.lua
+++ b/lib/skywalking/util.lua
@@ -1,34 +1,99 @@
--- 
+--
 -- Licensed to the Apache Software Foundation (ASF) under one or more
 -- contributor license agreements.  See the NOTICE file distributed with
 -- this work for additional information regarding copyright ownership.
 -- The ASF licenses this file to You under the Apache License, Version 2.0
 -- (the "License"); you may not use this file except in compliance with
 -- the License.  You may obtain a copy of the License at
--- 
+--
 --    http://www.apache.org/licenses/LICENSE-2.0
--- 
+--
 -- Unless required by applicable law or agreed to in writing, software
 -- distributed under the License is distributed on an "AS IS" BASIS,
 -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
--- 
+--
+local _M = {}
 
-local Util = {}
+-- for pure Lua
+local split = function(str, delimiter)
+    local t = {}
+    for substr in string.gmatch(str, "[^".. delimiter.. "]*") do
+        if substr ~= nil and string.len(substr) > 0 then
+            table.insert(t,substr)
+        end
+    end
+    return t
+end
+
+local timestamp = function()
+    local _, b = math.modf(os.clock())
+    if b==0 then
+        b='000'
+    else
+        b=tostring(b):sub(3,5)
+    end
+
+    return os.time() * 1000 + b
+end
+
+-- for Nginx Lua
+local ok, ngx_re = pcall(require, "ngx.re")
+if ok then
+    split = ngx_re.split
+    timestamp = function()
+        return ngx.now() * 1000
+    end
+end
+
+_M.split = split
+_M.timestamp = timestamp
+_M.is_ngx_lua = ok
+
 local MAX_ID_PART2 = 1000000000
 local MAX_ID_PART3 = 100000
-local SEQ = 1
 
-function Util:newID()
-    SEQ = SEQ + 1
-    return {Util.timestamp(), math.random( 0, MAX_ID_PART2), math.random( 0, MAX_ID_PART3) + SEQ}
+local random_seed = function ()
+    local seed
+    local frandom = io.open("/dev/urandom", "rb")
+    if frandom then
+        local str = frandom:read(4)
+        frandom:close()
+        if str then
+            local s = 0
+            for i = 1, 4 do
+                s = 256 * s + str:byte(i)
+            end
+            seed = s
+        end
+    end
+
+    if not seed then
+        if _M.is_ngx_lua then
+            seed = ngx.now() * 1000 + ngx.worker.pid()
+        else
+            seed = os.clock()
+        end
+    end
+
+    return seed
+end
+
+math.randomseed(random_seed())
+
+function _M.newID()
+    return {timestamp(), math.random(0, MAX_ID_PART2), math.random(0, MAX_ID_PART3)}
 end
 
 -- Format a trace/segment id into an array.
 -- An official ID should have three parts separated by '.' and each part of it is a number
-function Util:formatID(str) 
-    local parts = Util:split(str, '.')
+function _M.formatID(str)
+    local regex = '.'
+    if _M.is_ngx_lua then
+        regex = [[\.]]
+    end
+    local parts = split(str, regex)
     if #parts ~= 3 then
         return nil
     end
@@ -41,36 +106,8 @@
 end
 
 -- @param id is an array with length = 3
-function Util:id2String(id)
+function _M.id2String(id)
     return id[1] .. '.' .. id[2] .. '.' .. id[3]
 end
 
--- A simulation implementation of Java's System.currentTimeMillis() by following the SkyWalking protocol.
--- Return the difference as string, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC.
--- But in using os.clock(), I am not sure whether it is accurate enough.
-function Util:timestamp()
-    local a,b = math.modf(os.clock())
-    if b==0 then 
-        b='000' 
-    else 
-        b=tostring(b):sub(3,5) 
-    end
-
-    return os.time() * 1000 + b
-end
-
--- Split the given string by the delimiter. The delimiter should be a literal string, such as '.', '-'
-function Util:split(str, delimiter)
-    local t = {}
-
-    for substr in string.gmatch(str, "[^".. delimiter.. "]*") do
-        if substr ~= nil and string.len(substr) > 0 then
-            table.insert(t,substr)
-        end
-    end
-
-    return t
-end
-
-
-return Util
\ No newline at end of file
+return _M
diff --git a/lib/skywalking/util_test.lua b/lib/skywalking/util_test.lua
index e620017..10d8170 100644
--- a/lib/skywalking/util_test.lua
+++ b/lib/skywalking/util_test.lua
@@ -1,25 +1,25 @@
--- 
+--
 -- Licensed to the Apache Software Foundation (ASF) under one or more
 -- contributor license agreements.  See the NOTICE file distributed with
 -- this work for additional information regarding copyright ownership.
 -- The ASF licenses this file to You under the Apache License, Version 2.0
 -- (the "License"); you may not use this file except in compliance with
 -- the License.  You may obtain a copy of the License at
--- 
+--
 --    http://www.apache.org/licenses/LICENSE-2.0
--- 
+--
 -- Unless required by applicable law or agreed to in writing, software
 -- distributed under the License is distributed on an "AS IS" BASIS,
 -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
--- 
+--
 
 local lu = require('luaunit')
 local Util = require('util')
 
 TestUtil = {}
-    function TestUtil:testNewID()
+    function TestUtil.testNewID()
         local id = Util.newID()
 
         lu.assertNotNil(id[1])
@@ -27,11 +27,11 @@
         lu.assertNotNil(id[3])
     end
 
-    function TestUtil:testTimestamp()
+    function TestUtil.testTimestamp()
         local id = Util.timestamp()
         lu.assertNotNil(id)
     end
 -- end TestUtil
 
 
-os.exit( lu.LuaUnit.run() )
\ No newline at end of file
+os.exit( lu.LuaUnit.run() )
diff --git a/t/util.t b/t/util.t
new file mode 100644
index 0000000..53f6094
--- /dev/null
+++ b/t/util.t
@@ -0,0 +1,117 @@
+use Test::Nginx::Socket 'no_plan';
+
+use Cwd qw(cwd);
+my $pwd = cwd();
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+log_level('info');
+
+our $HttpConfig = qq{
+    lua_package_path "$pwd/lib/skywalking/?.lua;;";
+    error_log logs/error.log debug;
+    resolver 114.114.114.114 8.8.8.8 ipv6=off;
+    lua_shared_dict tracing_buffer 100m;
+};
+
+run_tests;
+
+__DATA__
+=== TEST 1: timestamp
+--- http_config eval: $::HttpConfig
+--- config
+    location /t {
+        content_by_lua_block {
+            local util = require('util')
+            local timestamp = util.timestamp()
+            local regex = [[^\d+$]]
+            local m = ngx.re.match(timestamp, regex)
+            if m and tonumber(m[0]) == timestamp then
+                ngx.say(true)
+            else
+                ngx.say(false)
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+true
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: newID
+--- http_config eval: $::HttpConfig
+--- config
+    location /t {
+        content_by_lua_block {
+            local util = require('util')
+            local new_id = util.newID()
+            local regex = [[^\d+$]]
+            ngx.say(#new_id)
+            for i = 1, #new_id, 1 do
+                local m = ngx.re.match(new_id[i], regex)
+                if m and tonumber(m[0]) == new_id[i] then
+                    ngx.say(i)
+                end
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+3
+1
+2
+3
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: id2String
+--- http_config eval: $::HttpConfig
+--- config
+    location /t {
+        content_by_lua_block {
+            local util = require('util')
+            local id = util.newID()
+            local id_str = util.id2String(id)
+            local regex = [[^\d+\.\d+\.\d+$]]
+            local m = ngx.re.match(id_str, regex)
+            if m then
+                ngx.say(true)
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+true
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: formatID
+--- http_config eval: $::HttpConfig
+--- config
+    location /t {
+        content_by_lua_block {
+            local util = require('util')
+            local id = util.newID()
+            local id_str = util.id2String(id)
+            local parts = util.formatID(id_str)
+            ngx.say(#parts)
+        }
+    }
+--- request
+GET /t
+--- response_body
+3
+--- no_error_log
+[error]