;; Tests for JMX support for Clojure

;; by Stuart Halloway

;; Copyright (c) Stuart Halloway. All rights reserved.  The use
;; and distribution terms for this software are covered by the Eclipse
;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this
;; distribution.  By using this software in any fashion, you are
;; agreeing to be bound by the terms of this license.  You must not
;; remove this notice, or any other, from this software.

(ns clojure.java.test-jmx
  (:import javax.management.openmbean.CompositeDataSupport
           [javax.management MBeanAttributeInfo Attribute AttributeList InstanceNotFoundException]
           [java.util.logging LogManager Logger])
  (:use clojure.test)
  (:require [clojure.java [jmx :as jmx]]))


(defn =set [a b]
  (= (set a) (set b)))

(defn seq-contains-all?
  "Does container contain every item in containee?
   Not fast. Testing use only"
  [container containee]
  (let [container (set container)]
    (every? #(contains? container %) containee)))

(deftest finding-mbeans
  (testing "as-object-name"
           (are [cname object-name]
                (= cname (.getCanonicalName object-name))
                "java.lang:type=Memory" (jmx/as-object-name "java.lang:type=Memory")))
  (testing "mbean-names"
           (are [cnames object-name]
                (= cnames (map #(.getCanonicalName %) object-name))
                ["java.lang:type=Memory"] (jmx/mbean-names "java.lang:type=Memory"))))

; These actual beans may differ on different JVM platforms.
; Tested April 2010 to work on Sun and IBM JDKs.
(deftest testing-actual-beans
  (testing "reflecting on capabilities"
    (are [attr-list mbean-name]
         (seq-contains-all? (jmx/attribute-names mbean-name) attr-list)
         [:Verbose :ObjectPendingFinalizationCount :HeapMemoryUsage :NonHeapMemoryUsage] "java.lang:type=Memory")
    (are [op-list mbean-name]
         (seq-contains-all? (jmx/operation-names mbean-name) op-list)
         [:gc] "java.lang:type=Memory"))
  (testing "mbean-from-oname"
    (are [key-names oname]
         (seq-contains-all? (keys (jmx/mbean oname)) key-names)
         [:Verbose :ObjectPendingFinalizationCount :HeapMemoryUsage :NonHeapMemoryUsage]  "java.lang:type=Memory")))

(deftest raw-reading-attributes
  (let [mem "java.lang:type=Memory"
        log "java.util.logging:type=Logging"]
    (testing "simple scalar attributes"
             (are [a b] (= a b)
                  false (jmx/raw-read mem :Verbose))
             (are [type attr] (instance? type attr)
                  Number (jmx/raw-read mem :ObjectPendingFinalizationCount)))
    (testing "reading multiple attributes"
             (are [a b] (= a b)
                  false ((jmx/raw-read mem [:Verbose]) :Verbose))
             (are [type attr] (instance? type attr)
                  Number ((jmx/raw-read mem [:ObjectPendingFinalizationCount]) :ObjectPendingFinalizationCount)))))

(deftest reading-attributes
  (testing "simple scalar attributes"
           (is (jmx/readable? "java.lang:type=Memory" :ObjectPendingFinalizationCount))
           (are [type attr] (instance? type attr)
                Number (jmx/read "java.lang:type=Memory" :ObjectPendingFinalizationCount)))
  (testing "composite attributes"
           (is (jmx/readable? "java.lang:type=Memory" :HeapMemoryUsage))
           (are [ks attr] (=set ks (keys attr))
                [:used :max :init :committed] (jmx/read "java.lang:type=Memory" :HeapMemoryUsage)))
  (testing "tabular attributes"
           (is (jmx/readable? "java.lang:type=Runtime" :SystemProperties))
           (is (map? (jmx/read "java.lang:type=Runtime" :SystemProperties)))))

(deftest reading-multiple-attributes
  (testing "simple scalar attributes"
           (are [type attr] (instance? type attr)
                Number ((jmx/read "java.lang:type=Memory" [:ObjectPendingFinalizationCount]) :ObjectPendingFinalizationCount)))
  (testing "composite attributes"
           (are [ks attr] (=set ks (keys attr))
                [:used :max :init :committed]
                ((jmx/read "java.lang:type=Memory" [:HeapMemoryUsage :NonHeapMemoryUsage]) :HeapMemoryUsage)))
  (testing "tabular attributes"
           (is (map? ((jmx/read "java.lang:type=Runtime" [:SystemProperties]) :SystemProperties)))))

(deftest writing-attributes
  (let [mem "java.lang:type=Memory"]
    (jmx/write! mem :Verbose true)
    (is (true? (jmx/read mem :Verbose)))
    (jmx/write! mem :Verbose false)
    (is (false? (jmx/read mem :Verbose)))))

(deftest test-invoke-operations
  (testing "without arguments"
           (jmx/invoke "java.lang:type=Memory" :gc))
  (testing "with arguments"
           (.addLogger (LogManager/getLogManager) (Logger/getLogger "clojure.java.test_jmx"))
           (jmx/invoke "java.util.logging:type=Logging" :setLoggerLevel "clojure.java.test_jmx" "WARNING"))
  (testing "with signature"
           (jmx/invoke-signature "java.lang:type=Threading" :getThreadInfo ["long"] 1)
           (jmx/invoke-signature "java.lang:type=Threading" :getThreadInfo ["long" "int"] 1 (new java.lang.Integer 1))))

(deftest test-objects->data
  (let [objects (jmx/raw-read "java.lang:type=Memory" :HeapMemoryUsage)
        prox (jmx/objects->data objects)]
    (testing "returns a map with keyword keys"
      (is (= (set [:committed :init :max :used]) (set (keys prox))))))
  (let [raw-props (jmx/raw-read "java.lang:type=Runtime" :SystemProperties)
        props (jmx/objects->data raw-props)]
    (are [k] (contains? props k)
         :java.class.path
         :path.separator))
  (testing "it works recursively on maps"
           (let [some-map {:foo (jmx/raw-read "java.lang:type=Memory" :HeapMemoryUsage)}]
             (is (map? (:foo (jmx/objects->data some-map))))))
  (testing "it handles null references"
    (is (nil? (jmx/objects->data nil))))
  (testing "it leaves everything else untouched"
    (is (= "foo" (jmx/objects->data "foo")))))

(deftest test-creating-attribute-infos
  (let [infos (@#'jmx/map->attribute-infos [[:a 1] [:b 2]])
        info (first infos)]
    (testing "generates the right class"
             (is (= (class (into-array MBeanAttributeInfo [])) (class infos))))
    (testing "generates the right instance data"
             (are [result expr] (= result expr)
                  "a" (.getName info)
                  "a" (.getDescription info)))))

(deftest various-beans-are-readable
  (testing "that all java.lang beans can be read without error"
           (doseq [mb (jmx/mbean-names "*:*")]
             (is (map? (jmx/mbean mb)) mb))))

(deftest test-jmx-url
  (testing "creates default url"
    (is (= "service:jmx:rmi:///jndi/rmi://localhost:3000/jmxrmi"
           (jmx/jmx-url))))
  (testing "creates custom url"
    (is (= "service:jmx:rmi:///jndi/rmi://example.com:4000/jmxrmi"
           (jmx/jmx-url {:host "example.com" :port 4000}))))
  (testing "creates custom jndi path"
    (is (= "service:jmx:rmi:///jndi/rmi://example.com:4000/jmxconnector"
           (jmx/jmx-url {:host "example.com" :port 4000 :jndi-path "jmxconnector"})))))

;; ----------------------------------------------------------------------
;; tests for clojure.java.jmx.Bean.

(deftest dynamic-mbean-from-compiled-class
  (let [mbean-name "clojure.java.test_jmx:name=Foo"]
    (jmx/register-mbean
     (jmx/create-bean
      (ref {:string-attribute "a-string"}))
     mbean-name)
    (are [result expr] (= result expr)
         "a-string" (jmx/read mbean-name :string-attribute)
         {:string-attribute "a-string"} (jmx/mbean mbean-name)
         )))

(deftest test-getAttribute
  (doseq [reftype [ref atom agent]]
    (let [state (reftype {:a 1 :b 2})
          bean (jmx/create-bean state)]
      (testing (str "accessing values from a " (class state))
        (are [result expr] (= result expr)
             1 (.getAttribute bean "a")
             2 (.getAttribute bean "b"))))))

(deftest test-setAttribute
  (doseq [reftype [ref atom agent]]
    (let [state (reftype {:a 1 :b 2})
          bean (jmx/create-bean state)]
      (testing (str "setting values on a " (class state))
        (.setAttribute bean (Attribute. "a" 3))
        (are [result expr] (= result expr)
             3 (.getAttribute bean "a")
             2 (.getAttribute bean "b"))))))

(deftest test-setAttributes
  (doseq [reftype [ref atom agent]]
    (let [state (reftype {:r 5 :d 4})
          bean (jmx/create-bean state)
          atts (.setAttributes bean (AttributeList. [(Attribute. "r" 6)
                                                    (Attribute. "d" 5)]))]
      (are [x y] (= x y)
          AttributeList (class atts)
          ["r" "d"] (map (memfn getName) (seq atts))
          [6 5] (map (memfn getValue) (seq atts))))))

(deftest test-bean-info
  (let [state (ref {:a 1 :b 2})
        bean (jmx/create-bean state)
        info (.getMBeanInfo bean)]
    (testing "accessing info"
             (are [result expr] (= result expr)
                  "clojure.java.jmx.Bean" (.getClassName info)))))

(deftest test-getAttributes
  (let [bean (jmx/create-bean (ref {:r 5 :d 4}))
        atts (.getAttributes bean (into-array ["r" "d"]))]
    (are [x y] (= x y)
         AttributeList (class atts)
         ["r" "d"] (map (memfn getName) (seq atts))
         [5 4] (map (memfn getValue) (seq atts)))))

(def primitive-int? (< (.compareTo (clojure-version) "1.3.0") 0))

(deftest test-guess-attribute-typename
  (are [x y] (= x (@#'jmx/guess-attribute-typename y))
       "boolean" false
       "java.lang.String" "foo"
       "long" (Long/valueOf (long 10)))
  (if primitive-int?
    (is (= "int" (@#'jmx/guess-attribute-typename 10)))
    (is (= "long" (@#'jmx/guess-attribute-typename 10)))))

(deftest test-unregister-mbean
  (let [mbean (jmx/create-bean (ref {:a-property 123}))
        mbean-name "clojure.java.test_jmx:name=UnregisterTest"]
    (jmx/register-mbean mbean mbean-name)
    (is (= 123 (jmx/read mbean-name :a-property)))
    (jmx/unregister-mbean mbean-name)
    (if (= [1 3] [(*clojure-version* :major) (*clojure-version* :minor)])
      ; clojure 1.3 throws RuntimeException directly for some reason so catch that and
      ; verify that the underlying exception is right
      (try
        (jmx/read mbean-name :a-property)
        (catch RuntimeException e
          (is (instance? InstanceNotFoundException (.getCause e)))))
      (is (thrown? InstanceNotFoundException (jmx/read mbean-name :a-property))))))
