Goal of this study

We are given a dataset with transactions of a set of products that are reported by the salespeople of some company. Our goal is to find “strange” transactions (aka outliers) that may indicate fraud attempts by some of the salespeople.

How to determine if a transaction is fraudulant

Some signs to look out for when determining whether a transaction is fraudulent:

A change in frequency of orders placed, for example, a customer who typically places a couple of orders a month, suddenly makes numerous transactions within a short span of time, sometimes within minutes of the previous order.   Orders that are significantly higher than a user’s average transaction.   Bulk orders of the same item with slight variations such as color or size—especially if this is atypical of the user’s transaction history.
A sudden change in delivery preference, for example, a change from home or office delivery address to in-store, warehouse, or PO Box delivery.
A mismatched IP Address, or an IP Address that is not from the general location or area of the billing address.

The Available Data

The data contains 401,146 rows and includes information on one sale by the salesperson. We have the following columns:
* ID - ID of the salesperson   * Prod - ID of the sold product
* Quant - number of reported sold units of the product
* Val - the reported total monetary value of the sale
* Insp - ok: if transaction is not fraudulant, fraud if it is, unkn if unknown

Importing the Data

library(dplyr)

Attaching package: ‘dplyr’

The following objects are masked from ‘package:stats’:

    filter, lag

The following objects are masked from ‘package:base’:

    intersect, setdiff, setequal, union
data(sales, package="DMwR2")
sales
Exploring the Dataset
Finding Number of unique values in the dataset
length(unique(unlist(sales[c("ID")])))
[1] 6014
length(unique(unlist(sales[c("Prod")])))
[1] 4546
length(unique(unlist(sales[c("Insp")])))
[1] 3
summary(sales)
       ID              Prod            Quant                Val             Insp       
 v431   : 10159   p1125  :  3923   Min.   :      100   Min.   :   1005   ok   : 14462  
 v54    :  6017   p3774  :  1824   1st Qu.:      107   1st Qu.:   1345   unkn :385414  
 v426   :  3902   p1437  :  1720   Median :      168   Median :   2675   fraud:  1270  
 v1679  :  3016   p1917  :  1702   Mean   :     8442   Mean   :  14617                 
 v1085  :  3001   p4089  :  1598   3rd Qu.:      738   3rd Qu.:   8680                 
 v1183  :  2642   p2742  :  1519   Max.   :473883883   Max.   :4642955                 
 (Other):372409   (Other):388860   NA's   :13842       NA's   :1182                    

To check if we have a significant number of products and salespeople we use the nlevels method (It return the number of levels which its argument has )

nlevels(sales$ID)
[1] 6016
nlevels(sales$Prod)
[1] 4548

Looking at the data where we have missing values for Quantity and Value

filter(sales,is.na(Quant),is.na(Val))

We can look at the proportions of the missing values

table(sales$Insp)/nrow(sales) * 100

       ok      unkn     fraud 
 3.605171 96.078236  0.316593 
library(ggplot2)
ggplot(group_by(sales,ID) %>% summarize(nTrans=n()),aes(x=ID,y=nTrans)) +
    geom_bar(stat="identity") + 
    theme(axis.text.x = element_blank(), axis.ticks.x=element_blank()) + 
    xlab("Salesmen") + ylab("Nr. of Transactions") +
    ggtitle("Nr. of Transactions per Salesman")
`summarise()` ungrouping output (override with `.groups` argument)

ggplot(group_by(sales,Prod) %>% summarize(nTrans=n()),aes(x=Prod,y=nTrans)) +
    geom_bar(stat="identity") + 
    theme(axis.text.x = element_blank(), axis.ticks.x=element_blank()) + 
    xlab("Product") + ylab("Nr. of Transactions") +
    ggtitle("Nr. of Transactions per Product")
`summarise()` ungrouping output (override with `.groups` argument)

ggplot(group_by(sales,ID) %>% summarize(nTrans=n()),aes(x=ID,y=nTrans)) +
    geom_bar(stat="identity") + 
    theme(axis.text.x = element_blank(), axis.ticks.x=element_blank()) + 
    xlab("Product") + ylab("Nr. of Transactions") +
    ggtitle("Nr. of Transactions per Salesman")
`summarise()` ungrouping output (override with `.groups` argument)

ggplot(group_by(sales,Prod) %>% summarize(nTrans=n()),aes(x=Prod,y=nTrans)) +
    geom_bar(stat="identity") + 
    theme(axis.text.x = element_blank(), axis.ticks.x=element_blank()) + 
    xlab("Salesmen") + ylab("Nr. of Transactions") +
    ggtitle("Nr. of Transactions per Product")
`summarise()` ungrouping output (override with `.groups` argument)

Now, we will add the unit price to the data set

sales <- mutate(sales,Uprice=Val/Quant)
sales

The variability should be lower for unit price

summary(sales$Uprice)
    Min.  1st Qu.   Median     Mean  3rd Qu.     Max.     NA's 
    0.00     8.46    11.89    20.30    19.11 26460.70    14136 

Seeing the data for lowest median unit price and highest median unit price.

prods <- group_by(sales,Prod)
mpProds <- summarize(prods,medianPrice=median(Uprice,na.rm=TRUE))
`summarise()` ungrouping output (override with `.groups` argument)
bind_cols(mpProds %>% arrange(medianPrice) %>% slice(1:5),
          mpProds %>% arrange(desc(medianPrice)) %>% slice(1:5))
New names:
* Prod -> Prod...1
* medianPrice -> medianPrice...2
* Prod -> Prod...3
* medianPrice -> medianPrice...4

We using the log scale because the scales of price of the most expensive and the least expensive products are different.

This shows the distribution of the unit prices of the cheapest and the most expensive products.

library(ggplot2)
library(forcats)
ggplot(filter(sales,Prod %in% c("p3689","p560")),aes(x=fct_drop(Prod),y=Uprice)) +
    geom_boxplot() + scale_y_log10() + 
    xlab("") + ylab("log10(UnitPrice)")

Similarly we will do this for the total value of the sale

ids <- group_by(sales,ID)
tvIDs <- summarize(ids,totalVal=sum(Val,na.rm=TRUE))
`summarise()` ungrouping output (override with `.groups` argument)
bind_cols(tvIDs %>% arrange(totalVal) %>% slice(1:5),
          tvIDs %>% arrange(desc(totalVal)) %>% slice(1:5))
New names:
* ID -> ID...1
* totalVal -> totalVal...2
* ID -> ID...3
* totalVal -> totalVal...4
arrange(tvIDs,desc(totalVal)) %>% slice(1:100) %>% 
    summarize(t100=sum(totalVal)) / 
    (summarize(tvIDs,sum(totalVal))) * 100
arrange(tvIDs,totalVal) %>% slice(1:2000) %>% 
    summarize(b2000=sum(totalVal)) / 
    (summarize(tvIDs,sum(totalVal))) * 100

Now, we’ll look at the same analysis for quantity

prods <- group_by(sales,Prod)
qtProds <- summarize(prods,totalQty=sum(Quant,na.rm=TRUE))
`summarise()` ungrouping output (override with `.groups` argument)
bind_cols(qtProds %>% arrange(desc(totalQty)) %>% slice(1:5),
          qtProds %>% arrange(totalQty) %>% slice(1:5))
New names:
* Prod -> Prod...1
* totalQty -> totalQty...2
* Prod -> Prod...3
* totalQty -> totalQty...4
arrange(qtProds,desc(totalQty)) %>% slice(1:100) %>% 
    summarize(t100=sum(as.numeric(totalQty))) / 
    (summarize(qtProds,sum(as.numeric(totalQty)))) * 100
arrange(qtProds,totalQty) %>% slice(1:4000) %>% 
    summarize(b4000=sum(as.numeric(totalQty))) / 
    (summarize(qtProds,sum(as.numeric(totalQty)))) * 100
nouts <- function(x) length(boxplot.stats(x)$out)
noutsProds <- summarise(prods,nOut=nouts(Uprice))
`summarise()` ungrouping output (override with `.groups` argument)

This result below is showing the number of “strange” transactions (outliers) for each product

arrange(noutsProds,desc(nOut))

Total number of outliers

summarize(noutsProds,totalOuts=sum(nOut))
summarize(noutsProds,totalOuts=sum(nOut))/nrow(sales)*100

Data Problems

Missing values of the data set

This returns the salespeople with a larger proportion of transactions of unknowns on both Val and Quantity

prop.naQandV <- function(q,v) 100*sum(is.na(q) & is.na(v))/length(q)
summarise(ids,nProbs=prop.naQandV(Quant,Val)) %>% arrange(desc(nProbs))
`summarise()` ungrouping output (override with `.groups` argument)

Similarly, products with highest proportions of missing values

summarise(prods,nProbs=prop.naQandV(Quant,Val)) %>% arrange(desc(nProbs))
`summarise()` ungrouping output (override with `.groups` argument)

removing transactions with unknown values for Quantity and Value

sales <- filter(sales,!(is.na(Quant) & is.na(Val)))
prop.nas <- function(x) 100*sum(is.na(x))/length(x)
summarise(prods,propNA.Q=prop.nas(Quant)) %>% arrange(desc(propNA.Q))
`summarise()` ungrouping output (override with `.groups` argument)
filter(sales, Prod %in% c("p2442","p2443")) %>% 
    group_by(Insp) %>% count()
sales <- droplevels(filter(sales,!(Prod %in% c("p2442", "p2443"))))
summarise(ids,propNA.Q=prop.nas(Quant)) %>% arrange(desc(propNA.Q))
`summarise()` ungrouping output (override with `.groups` argument)
summarise(prods,propNA.V=prop.nas(Val)) %>% arrange(desc(propNA.V))
`summarise()` ungrouping output (override with `.groups` argument)
summarise(ids,propNA.V=prop.nas(Val)) %>% arrange(desc(propNA.V))
`summarise()` ungrouping output (override with `.groups` argument)
tPrice <- filter(sales, Insp != "fraud") %>% 
          group_by(Prod) %>% 
          summarise(medianPrice = median(Uprice,na.rm=TRUE))
`summarise()` ungrouping output (override with `.groups` argument)
noQuantMedPrices <- filter(sales, is.na(Quant)) %>% 
    inner_join(tPrice) %>% 
    select(medianPrice)
Joining, by = "Prod"
noValMedPrices <- filter(sales, is.na(Val)) %>% 
    inner_join(tPrice) %>% 
    select(medianPrice)
Joining, by = "Prod"
noQuant <- which(is.na(sales$Quant))
noVal <- which(is.na(sales$Val))
sales[noQuant,'Quant'] <- ceiling(sales[noQuant,'Val'] /noQuantMedPrices)
sales[noVal,'Val'] <- sales[noVal,'Quant'] * noValMedPrices
sales$Uprice <- sales$Val/sales$Quant
save(sales, file = "salesClean.Rdata")
ms <- filter(sales,Insp != "fraud") %>% 
    group_by(Prod) %>% 
    summarize(median=median(Uprice,na.rm=TRUE),
              iqr=IQR(Uprice,na.rm=TRUE),
              nTrans=n(),
              fewTrans=ifelse(nTrans>20,FALSE,TRUE))
`summarise()` ungrouping output (override with `.groups` argument)
ms

Properties of the distribution of unit prices

ggplot(ms,aes(x=median,y=iqr,color=fewTrans)) + 
    geom_point() + 
    xlab("Median") + ylab("IQR")

ggplot(ms,aes(x=median,y=iqr,color=fewTrans)) + 
    geom_point() + 
    scale_y_log10() + scale_x_log10() + 
    xlab("log(Median)") + ylab("log(IQR)")

ms <- mutate(ms,smedian=scale(median),siqr=scale(iqr))
smalls <- which(ms$fewTrans)
nsmalls <- as.character(ms$Prod[smalls])
similar <- matrix(NA,length(smalls),7,
    dimnames=list(nsmalls,
      c("RowSimProd", "ks.stat", "ks.p", "medP", "iqrP", "medS","iqrS")))
xprods <- tapply(sales$Uprice, sales$Prod, list)
for(i in seq_along(smalls)) {
    d <- scale(ms[,c("smedian","siqr")],
               c(ms$smedian[smalls[i]],ms$siqr[smalls[i]]),
               FALSE)
    d <- sqrt(drop(d^2 %*% rep(1, ncol(d))))
    stat <- ks.test(xprods[[nsmalls[i]]], xprods[[order(d)[2]]])
    similar[i, ] <- c(order(d)[2], stat$statistic, stat$p.value,
                      ms$median[smalls[i]],ms$iqr[smalls[i]],
                      ms$median[order(d)[2]],ms$iqr[order(d)[2]])
}
head(similar)
    RowSimProd   ks.stat       ks.p     medP      iqrP     medS      iqrS
p8        2827 0.4339623 0.06470603 3.850211 0.7282168 3.868306 0.7938557
p18        213 0.2568922 0.25815859 5.187266 8.0359968 5.274884 7.8207052
p38       1044 0.3650794 0.11308315 5.490758 6.4162095 5.651818 6.2436224
p39       3418 0.2214286 0.81418197 7.986486 1.4229755 8.005181 1.5625650
p40       1335 0.3760000 0.04533293 9.674797 1.6104511 9.711538 1.6505602
p47       1387 0.3125000 0.48540576 2.504092 2.5625835 2.413498 2.6402087
bind_rows(filter(ms,Prod==rownames(similar)[1]),
          ms[similar[1,1],])
nrow(similar[similar[, "ks.p"] >= 0.9, ])
[1] 140
sum(similar[, "ks.p"] >= 0.9)
[1] 140
save(similar, file = "similarProducts.Rdata")

Defining the Data Mining Tasks

Evaluation Criteria

library(ROCR)
data(ROCR.simple)
pred <- prediction(ROCR.simple$predictions, ROCR.simple$labels)
perf <- performance(pred, "prec", "rec")
plot(perf)

PRcurve <- function(preds, trues, ...) {
    require(ROCR, quietly = TRUE)
    pd <- prediction(preds, trues)
    pf <- performance(pd, "prec", "rec")
    pf@y.values <- lapply(pf@y.values, function(x) rev(cummax(rev(x))))
    plot(pf, ...)
}
PRcurve(ROCR.simple$predictions, ROCR.simple$labels)

library(ROCR)
data(ROCR.simple)
pred <- prediction(ROCR.simple$predictions, ROCR.simple$labels)
perf <- performance(pred, "prec", "rec")
par( mfrow=c(1,2) )
plot(perf)
PRcurve(ROCR.simple$predictions, ROCR.simple$labels)
par( mfrow=c(1,1) )

pred <- prediction(ROCR.simple$predictions, ROCR.simple$labels)
perf <- performance(pred, "lift", "rpp")
plot(perf, main = "Lift Chart")

CRchart <- function(preds, trues, ...) {
    require(ROCR, quietly = T)
    pd <- prediction(preds, trues)
    pf <- performance(pd, "rec", "rpp")
    plot(pf, ...)
}
CRchart(ROCR.simple$predictions, ROCR.simple$labels, 
        main='Cumulative Recall Chart')

avgNDTP <- function(toInsp,train,stats) {
  if (missing(train) && missing(stats)) 
      stop('Provide either the training data or the product stats')
  if (missing(stats)) {
      stats <- as.matrix(filter(train,Insp != 'fraud') %>%
                         group_by(Prod) %>%
                         summarise(median=median(Uprice),iqr=IQR(Uprice)) %>%
                         select(median,iqr))
      rownames(stats) <- levels(train$Prod)
      stats[which(stats[,'iqr']==0),'iqr'] <- stats[which(stats[,'iqr']==0),'median']
  }
  
  return(mean(abs(toInsp$Uprice-stats[toInsp$Prod,'median']) /
                 stats[toInsp$Prod,'iqr']))
}
Experimental Methodology
evalOutlierRanking <- function(testSet,rankOrder,Threshold,statsProds,...) 
{
   ordTS <- testSet[rankOrder,]
   N <- nrow(testSet)
   nF <- if (Threshold < 1) as.integer(Threshold*N) else Threshold
   cm <- table(c(rep('fraud',nF),rep('ok',N-nF)),ordTS$Insp)
   prec <- cm['fraud','fraud']/sum(cm['fraud',])
   rec <- cm['fraud','fraud']/sum(cm[,'fraud'])
   AVGndtp <- avgNDTP(ordTS[1:nF,],stats=statsProds)
   return(c(Precision=prec,Recall=rec,avgNDTP=AVGndtp))
}

Obtaining Outlier Rankings

Unsupervised Approaches

BPrule.wf <- function(form,train,test,...) {
    require(dplyr, quietly=TRUE)
    ms <- as.matrix(filter(train,Insp != 'fraud') %>%
                    group_by(Prod) %>%
                    summarise(median=median(Uprice),iqr=IQR(Uprice)) %>%
                    select(median,iqr))
    rownames(ms) <- levels(train$Prod)
    ms[which(ms[,'iqr']==0),'iqr'] <- ms[which(ms[,'iqr']==0),'median']
    ORscore <- abs(test$Uprice-ms[test$Prod,'median']) /
               ms[test$Prod,'iqr']
    rankOrder <- order(ORscore,decreasing=TRUE)
    res <- list(testSet=test,rankOrder=rankOrder,
                probs=matrix(c(ORscore,ifelse(test$Insp=='fraud',1,0)),
                             ncol=2))
    res
}
library(dplyr)
globalStats <- as.matrix(filter(sales,Insp != 'fraud') %>%
                         group_by(Prod) %>%
                         summarise(median=median(Uprice),iqr=IQR(Uprice)) %>%
                         select(median,iqr))
`summarise()` ungrouping output (override with `.groups` argument)
rownames(globalStats) <- levels(sales$Prod)
globalStats[which(globalStats[,'iqr']==0),'iqr'] <- 
    globalStats[which(globalStats[,'iqr']==0),'median']
head(globalStats,3)
     median      iqr
p1 11.34615 8.563580
p2 10.87786 5.609731
p3 10.00000 4.809092
library(performanceEstimation)
bp.res <- performanceEstimation(
    PredTask(Insp ~ ., sales),
    Workflow("BPrule.wf"),
    EstimationTask(metrics=c("Precision","Recall","avgNDTP"),
                   method=Holdout(nReps=3, hldSz=0.3, strat=TRUE),
                   evaluator="evalOutlierRanking",
                   evaluator.pars=list(Threshold=0.1, statsProds=globalStats))
)


##### PERFORMANCE ESTIMATION USING  HOLD OUT  #####

** PREDICTIVE TASK :: sales.Insp

++ MODEL/WORKFLOW :: BPrule.wf 
Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 
Iteration :  1
`summarise()` ungrouping output (override with `.groups` argument)
  2
`summarise()` ungrouping output (override with `.groups` argument)
  3
`summarise()` ungrouping output (override with `.groups` argument)
summary(bp.res)

== Summary of a  Hold Out Performance Estimation Experiment ==

Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 

* Predictive Tasks ::  sales.Insp
* Workflows  ::  BPrule.wf 

-> Task:  sales.Insp
  *Workflow: BPrule.wf 
           Precision     Recall    avgNDTP
avg     0.0178244211 0.56315789 11.3639416
std     0.0007261201 0.02294157  0.7633666
med     0.0181575879 0.57368421 11.1037531
iqr     0.0006663335 0.02105263  0.7293525
min     0.0169915042 0.53684211 10.7646833
max     0.0183241712 0.57894737 12.2233884
invalid 0.0000000000 0.00000000  0.0000000
ps.bp <- sapply(getIterationsInfo(bp.res),function(i) i$probs[,1])
ts.bp <- sapply(getIterationsInfo(bp.res),function(i) i$probs[,2])
PRcurve(ps.bp,ts.bp,main="PR curve",avg="vertical")

CRchart(ps.bp,ts.bp,main='Cumulative Recall curve',avg='vertical')

par(mfrow=c(1,2)) 
ps.bp <- sapply(getIterationsInfo(bp.res), function(i) i$probs[,1])
ts.bp <- sapply(getIterationsInfo(bp.res), function(i) i$probs[,2])
PRcurve(ps.bp, ts.bp, main="PR curve", avg="vertical")
CRchart(ps.bp, ts.bp, main='Cumulative Recall curve', avg='vertical')

LOF.wf <- function(form, train, test, k, ...) {
    require(DMwR2, quietly=TRUE)
    ntr <- nrow(train)
    all <- as.data.frame(rbind(train,test))
    N <- nrow(all)
    ups <- split(all$Uprice,all$Prod)
    r <- list(length=ups)
    for(u in seq(along=ups)) 
        r[[u]] <- if (NROW(ups[[u]]) > 3) 
                      lofactor(ups[[u]],min(k,NROW(ups[[u]]) %/% 2)) 
                  else if (NROW(ups[[u]])) rep(0,NROW(ups[[u]])) 
                  else NULL
    all$lof <- vector(length=N)
    split(all$lof,all$Prod) <- r
    all$lof[which(!(is.infinite(all$lof) | is.nan(all$lof)))] <- 
        SoftMax(all$lof[which(!(is.infinite(all$lof) | is.nan(all$lof)))])
    
    res <- list(testSet=test,
                rankOrder=order(all[(ntr+1):N,'lof'],decreasing=TRUE),
                probs=as.matrix(cbind(all[(ntr+1):N,'lof'],
                                      ifelse(test$Insp=='fraud',1,0))))
    res
}
lof.res <- performanceEstimation(
    PredTask(Insp ~ . , sales),
    Workflow("LOF.wf", k=7),
    EstimationTask(metrics=c("Precision","Recall","avgNDTP"),
                   method=Holdout(nReps=3, hldSz=0.3, strat=TRUE),
                   evaluator="evalOutlierRanking",
                   evaluator.pars=list(Threshold=0.1, statsProds=globalStats))
    )


##### PERFORMANCE ESTIMATION USING  HOLD OUT  #####

** PREDICTIVE TASK :: sales.Insp

++ MODEL/WORKFLOW :: LOF.wf 
Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 
Iteration :  1
Registered S3 method overwritten by 'quantmod':
  method            from
  as.zoo.data.frame zoo 

Attaching package: ‘DMwR2’

The following object is masked _by_ ‘.GlobalEnv’:

    sales
  2  3
summary(lof.res)

== Summary of a  Hold Out Performance Estimation Experiment ==

Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 

* Predictive Tasks ::  sales.Insp
* Workflows  ::  LOF.wf 

-> Task:  sales.Insp
  *Workflow: LOF.wf 
           Precision      Recall   avgNDTP
avg     0.0225442834 0.712280702 8.9724516
std     0.0002925108 0.009241802 0.7111686
med     0.0225720473 0.713157895 8.7667068
iqr     0.0002915209 0.009210526 0.6884858
min     0.0222388806 0.702631579 8.3868382
max     0.0228219224 0.721052632 9.7638097
invalid 0.0000000000 0.000000000 0.0000000
ps.lof <- sapply(getIterationsInfo(lof.res), function(i) i$probs[,1])
ts.lof <- sapply(getIterationsInfo(lof.res), function(i) i$probs[,2])
PRcurve(ps.bp,ts.bp,main="PR curve",lty=1,
        xlim=c(0,1),ylim=c(0,1),avg="vertical")

PRcurve(ps.lof,ts.lof,add=TRUE,lty=2,avg='vertical')
Error: 'predictions' contains NA.
par(mfrow=c(1,2)) 
ps.lof <- sapply(getIterationsInfo(lof.res), function(i) i$probs[,1])
ts.lof <- sapply(getIterationsInfo(lof.res), function(i) i$probs[,2])
PRcurve(ps.bp, ts.bp,main="PR curve",lty=1,
        xlim=c(0,1),ylim=c(0,1),avg="vertical")

PRcurve(ps.lof, ts.lof,add=T,lty=2,avg='vertical')
Error: 'predictions' contains NA.
ORh.wf <- function(form, train, test, ...) {
    require(DMwR2, quietly=TRUE)
    ntr <- nrow(train)
    all <- as.data.frame(rbind(train,test))
    N <- nrow(all)
    ups <- split(all$Uprice,all$Prod)
    r <- list(length=ups)
    for(u in seq(along=ups)) 
        r[[u]] <- if (NROW(ups[[u]]) > 3) 
                      outliers.ranking(ups[[u]])$prob.outliers
                  else if (NROW(ups[[u]])) rep(0,NROW(ups[[u]])) 
                  else NULL
    all$orh <- vector(length=N)
    split(all$orh,all$Prod) <- r
    all$orh[which(!(is.infinite(all$orh) | is.nan(all$orh)))] <- 
        SoftMax(all$orh[which(!(is.infinite(all$orh) | is.nan(all$orh)))])
    res <- list(testSet=test,
                rankOrder=order(all[(ntr+1):N,'orh'],decreasing=TRUE),
                probs=as.matrix(cbind(all[(ntr+1):N,'orh'],
                                      ifelse(test$Insp=='fraud',1,0))))
    res
    
}
orh.res <- performanceEstimation(
    PredTask(Insp ~ . , sales),
    Workflow("ORh.wf"),
    EstimationTask(metrics=c("Precision","Recall","avgNDTP"),
                   method=Holdout(nReps=3, hldSz=0.3, strat=TRUE),
                   evaluator="evalOutlierRanking",
                   evaluator.pars=list(Threshold=0.1, statsProds=globalStats))
    )


##### PERFORMANCE ESTIMATION USING  HOLD OUT  #####

** PREDICTIVE TASK :: sales.Insp

++ MODEL/WORKFLOW :: ORh.wf 
Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 
Iteration :  1  2  3
summary(orh.res)

== Summary of a  Hold Out Performance Estimation Experiment ==

Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 

* Predictive Tasks ::  sales.Insp
* Workflows  ::  ORh.wf 

-> Task:  sales.Insp
  *Workflow: ORh.wf 
           Precision     Recall    avgNDTP
avg     0.0222388806 0.70263158  9.1207378
std     0.0006006249 0.01897659  0.9773273
med     0.0220722972 0.69736842  8.6315065
iqr     0.0005830418 0.01842105  0.8807146
min     0.0217391304 0.68684211  8.4846389
max     0.0229052141 0.72368421 10.2460681
invalid 0.0000000000 0.00000000  0.0000000
ps.orh <- sapply(getIterationsInfo(orh.res), function(i) i$probs[,1])
ts.orh <- sapply(getIterationsInfo(orh.res), function(i) i$probs[,2])
PRcurve(ps.bp,ts.bp,main="PR curve",lty=1,
        xlim=c(0,1),ylim=c(0,1),avg="vertical")

PRcurve(ps.lof,ts.lof,add=TRUE,lty=2,avg='vertical')
Error: 'predictions' contains NA.
par(mfrow=c(1,2)) 
ps.orh <- sapply(getIterationsInfo(orh.res), function(i) i$probs[,1])
ts.orh <- sapply(getIterationsInfo(orh.res), function(i) i$probs[,2])
PRcurve(ps.bp,ts.bp,main="PR curve",lty=1,
        xlim=c(0,1),ylim=c(0,1),avg="vertical")

PRcurve(ps.lof,ts.lof,add=T,lty=2,avg='vertical')
Error: 'predictions' contains NA.

Supervised Approaches

library(UBL)
Loading required package: MBA
Loading required package: gstat
Loading required package: automap
Loading required package: sp
Loading required package: randomForest
randomForest 4.6-14
Type rfNews() to see new features/changes/bug fixes.

Attaching package: ‘randomForest’

The following object is masked from ‘package:ggplot2’:

    margin

The following object is masked from ‘package:dplyr’:

    combine
data(iris)
data <- iris[, c(1, 2, 5)]
data$Species <- factor(ifelse(data$Species == "setosa", "rare","common"))
table(data$Species)

common   rare 
   100     50 
newData <- SmoteClassif(Species ~ ., data, C.perc = "balance")
table(newData$Species)

common   rare 
    75     75 
newData2 <- SmoteClassif(Species ~ ., data, C.perc = list(common = 1,rare = 6))
table(newData2$Species)

common   rare 
   100    300 
library(ggplot2)
ggplot(data,aes(x=Sepal.Length,y=Sepal.Width,color=Species)) +
    geom_point() + ggtitle("Original Data")

ggplot(newData2,aes(x=Sepal.Length,y=Sepal.Width,color=Species)) +
    geom_point() + ggtitle("SMOTE'd Data")

NB.wf <- function(form,train,test,...) {
    require(e1071,quietly=TRUE)
    sup <- which(train$Insp != 'unkn')
    data <- as.data.frame(train[sup,c('ID','Prod','Uprice','Insp')])
    data$Insp <- factor(data$Insp,levels=c('ok','fraud'))
    model <- naiveBayes(Insp ~ .,data, ...)
    preds <- predict(model,test[,c('ID','Prod','Uprice','Insp')], type='raw')
    rankOrder <- order(preds[,'fraud'], decreasing=TRUE)
    rankScore <- preds[,'fraud']
    res <- list(testSet=test,
                rankOrder=rankOrder,
                probs=as.matrix(cbind(rankScore,
                                ifelse(test$Insp=='fraud',1,0))))
    res
}
nb.res <- performanceEstimation(
    PredTask(Insp ~ . , sales),
    Workflow("NB.wf"),
    EstimationTask(metrics=c("Precision","Recall","avgNDTP"),
                   method=Holdout(nReps=3,hldSz=0.3,strat=TRUE),
                   evaluator="evalOutlierRanking",
                   evaluator.pars=list(Threshold=0.1,
                                       statsProds=globalStats))
    )


##### PERFORMANCE ESTIMATION USING  HOLD OUT  #####

** PREDICTIVE TASK :: sales.Insp

++ MODEL/WORKFLOW :: NB.wf 
Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 
Iteration :  1  2  3
summary(nb.res)

== Summary of a  Hold Out Performance Estimation Experiment ==

Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 

* Predictive Tasks ::  sales.Insp
* Workflows  ::  NB.wf 

-> Task:  sales.Insp
  *Workflow: NB.wf 
           Precision     Recall  avgNDTP
avg     0.0134932534 0.42631579 6.403751
std     0.0009009374 0.02846488 1.070227
med     0.0137431284 0.43421053 6.175571
iqr     0.0008745627 0.02763158 1.051825
min     0.0124937531 0.39473684 5.466015
max     0.0142428786 0.45000000 7.569665
invalid 0.0000000000 0.00000000 0.000000
ps.nb <- sapply(getIterationsInfo(nb.res), function(i) i$probs[,1])
ts.nb <- sapply(getIterationsInfo(nb.res), function(i) i$probs[,2])
PRcurve(ps.nb,ts.nb,main="PR curve",lty=1,
        xlim=c(0,1),ylim=c(0,1),avg="vertical")
PRcurve(ps.orh,ts.orh,add=TRUE,lty=2,avg='vertical')
legend('topright',c('NaiveBayes','ORh'),lty=1,col=c('black','grey'))


CRchart(ps.nb,ts.nb,main='Cumulative Recall curve',
        lty=1,xlim=c(0,1),ylim=c(0,1),avg='vertical')
CRchart(ps.orh,ts.orh,add=TRUE,lty=2,avg='vertical')
legend('bottomright',c('NaiveBayes','ORh'),lty=1,col=c('black','grey'))

par(mfrow=c(1,2)) 
ps.nb <- sapply(getIterationsInfo(nb.res), function(i) i$probs[,1])
ts.nb <- sapply(getIterationsInfo(nb.res), function(i) i$probs[,2])
PRcurve(ps.nb,ts.nb,main="PR curve",lty=1,
        xlim=c(0,1),ylim=c(0,1),avg="vertical")
PRcurve(ps.orh,ts.orh,add=T,lty=2,avg='vertical')
legend('topright',c('NaiveBayes','ORh'),lty=1,col=c('black','grey'))

CRchart(ps.nb,ts.nb,main='Cumulative Recall curve',
        lty=1,xlim=c(0,1),ylim=c(0,1),avg='vertical')
CRchart(ps.orh,ts.orh,add=T,lty=2,avg='vertical')
legend('bottomright',c('NaiveBayes','ORh'),lty=1,col=c('black','grey'))

NBsm.wf <- function(form,train,test,C.perc="balance",dist="HEOM",...) {
    require(e1071,quietly=TRUE)
    require(UBL,quietly=TRUE)

    sup <- which(train$Insp != 'unkn')
    data <- as.data.frame(train[sup,c('ID','Prod','Uprice','Insp')])
    data$Insp <- factor(data$Insp,levels=c('ok','fraud'))
    newData <- SmoteClassif(Insp ~ .,data,C.perc=C.perc,dist=dist,...)
    model <- naiveBayes(Insp ~ .,newData)
    preds <- predict(model,test[,c('ID','Prod','Uprice','Insp')],type='raw')
    rankOrder <- order(preds[,'fraud'],decreasing=T)
    rankScore <- preds[,'fraud']
    
    res <- list(testSet=test,
              rankOrder=rankOrder,
              probs=as.matrix(cbind(rankScore,
                                    ifelse(test$Insp=='fraud',1,0))))
    res
}
nbs.res <- performanceEstimation(
    PredTask(Insp ~ ., sales),
    Workflow("NBsm.wf"),
    EstimationTask(metrics=c("Precision","Recall","avgNDTP"),
                   method=Holdout(nReps=3,hldSz=0.3,strat=TRUE),
                   evaluator="evalOutlierRanking",
                   evaluator.pars=list(Threshold=0.1,
                                       statsProds=globalStats))
    )


##### PERFORMANCE ESTIMATION USING  HOLD OUT  #####

** PREDICTIVE TASK :: sales.Insp

++ MODEL/WORKFLOW :: NBsm.wf 
Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 
Iteration :  1  2  3
summary(nbs.res) 

== Summary of a  Hold Out Performance Estimation Experiment ==

Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 

* Predictive Tasks ::  sales.Insp
* Workflows  ::  NBsm.wf 

-> Task:  sales.Insp
  *Workflow: NBsm.wf 
           Precision     Recall   avgNDTP
avg     0.0145205175 0.45877193 6.7723410
std     0.0009866931 0.03117431 0.8098474
med     0.0139930035 0.44210526 6.3507932
iqr     0.0008745627 0.02763158 0.7228941
min     0.0139097118 0.43947368 6.2602207
max     0.0156588372 0.49473684 7.7060090
invalid 0.0000000000 0.00000000 0.0000000
ps.nbs <- sapply(getIterationsInfo(nbs.res), function(i) i$probs[,1])
ts.nbs <- sapply(getIterationsInfo(nbs.res), function(i) i$probs[,2])
PRcurve(ps.nb,ts.nb,main="PR curve",lty=1,
        xlim=c(0,1),ylim=c(0,1), avg="vertical")
PRcurve(ps.orh,ts.orh,add=TRUE,lty=2, avg='vertical')
PRcurve(ps.nbs,ts.nbs,add=TRUE,lty=1, col='grey',avg='vertical')
legend('topright',c('NaiveBayes','ORh','smoteNaiveBayes'),lty=c(1,2,1),
       col=c('black','black','grey'))


CRchart(ps.nb,ts.nb,main='Cumulative Recall curve',
        lty=1,xlim=c(0,1),ylim=c(0,1),avg='vertical')
CRchart(ps.orh,ts.orh,add=TRUE,lty=2,avg='vertical')
CRchart(ps.nbs,ts.nbs,add=TRUE,lty=1,col='grey',avg='vertical')
legend('bottomright',c('NaiveBayes','ORh','smoteNaiveBayes'),lty=c(1,2,1),
       col=c('black','black','grey'))

par(mfrow=c(1,2)) 
ps.nbs <- sapply(getIterationsInfo(nbs.res),  function(i) i$probs[,1])
ts.nbs <- sapply(getIterationsInfo(nbs.res),  function(i) i$probs[,2])
PRcurve(ps.nb,ts.nb,main="PR curve",lty=1,
        xlim=c(0,1),ylim=c(0,1), avg="vertical")
PRcurve(ps.orh,ts.orh,add=T,lty=2,avg='vertical')
PRcurve(ps.nbs,ts.nbs,add=T,lty=1,col='grey',avg='vertical')
legend('topright',c('NaiveBayes','ORh','smoteNaiveBayes'),
       lty=c(1,2,1),col=c('black','black','grey'))

CRchart(ps.nb,ts.nb,main='Cumulative Recall curve',
        lty=1,xlim=c(0,1),ylim=c(0,1),avg='vertical')
CRchart(ps.orh,ts.orh,add=T,lty=2,avg='vertical')
CRchart(ps.nbs,ts.nbs,add=T,lty=1,col='grey',avg='vertical')
legend('bottomright',c('NaiveBayes','ORh','smoteNaiveBayes'),
       lty=c(1,2,1),col=c('black','black','grey'))

Semi-Supervised Approaches

library(DMwR2)
library(e1071)
data(iris) 
set.seed(1234)
idx <- sample(150, 100)
tr <- iris[idx, ]
ts <- iris[-idx, ]
nb <- naiveBayes(Species ~ ., tr)
table(predict(nb, ts), ts$Species)
            
             setosa versicolor virginica
  setosa         18          0         0
  versicolor      0         16         1
  virginica       0          2        13
trST <- tr
nas <- sample(100, 90)
trST[nas, "Species"] <- NA
func <- function(m, d) {
    p <- predict(m, d, type = "raw")
    data.frame(cl = colnames(p)[ apply(p, 1, which.max) ], 
               p = apply(p, 1, max))
}
nbSTbase <- naiveBayes(Species ~ ., trST[-nas, ])
table(predict(nbSTbase, ts), ts$Species)
            
             setosa versicolor virginica
  setosa         18          0         0
  versicolor      0         16         2
  virginica       0          2        12
nbST <- SelfTrain(Species ~ ., trST, 
                  learner="naiveBayes", learner.pars=list(),
                  pred="func")
table(predict(nbST, ts), ts$Species)
            
             setosa versicolor virginica
  setosa         18          0         0
  versicolor      0         16         1
  virginica       0          2        13
pred.nb <- function(m,d) {
    p <- predict(m,d,type='raw')
    data.frame(cl=colnames(p)[apply(p,1,which.max)],
               p=apply(p,1,max)
               )
}
 
nb.st.wf <- function(form,train,test,...) {
    require(e1071,quietly=TRUE)
    require(DMwR2, quietly=TRUE)
    train <- as.data.frame(train[,c('ID','Prod','Uprice','Insp')])
    train[which(train$Insp == 'unkn'),'Insp'] <- NA
    train$Insp <- factor(train$Insp,levels=c('ok','fraud'))
    model <- SelfTrain(form,train,
                       learner='naiveBayes', learner.pars=list(),
                       pred='pred.nb')
    preds <- predict(model,test[,c('ID','Prod','Uprice','Insp')],
                     type='raw')

    rankOrder <- order(preds[,'fraud'],decreasing=TRUE)
    rankScore <- preds[,"fraud"]
    
    res <- list(testSet=test,
              rankOrder=rankOrder,
              probs=as.matrix(cbind(rankScore,
                                    ifelse(test$Insp=='fraud',1,0))))
    res
}
nb.st.res <- performanceEstimation(
    PredTask(Insp ~ .,sales),
    Workflow("nb.st.wf"),
    EstimationTask(metrics=c("Precision","Recall","avgNDTP"),
                   method=Holdout(nReps=3,hldSz=0.3,strat=TRUE),
                   evaluator="evalOutlierRanking",
                   evaluator.pars=list(Threshold=0.1,
                                       statsProds=globalStats))
    )


##### PERFORMANCE ESTIMATION USING  HOLD OUT  #####

** PREDICTIVE TASK :: sales.Insp

++ MODEL/WORKFLOW :: nb.st.wf 
Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 
Iteration :  1  2  3
summary(nb.st.res)

== Summary of a  Hold Out Performance Estimation Experiment ==

Task for estimating  Precision,Recall,avgNDTP  using
Stratified  3 x 70 % / 30 % Holdout
     Run with seed =  1234 

* Predictive Tasks ::  sales.Insp
* Workflows  ::  nb.st.wf 

-> Task:  sales.Insp
  *Workflow: nb.st.wf 
           Precision      Recall   avgNDTP
avg     0.0132989061 0.420175439 7.1697293
std     0.0002544603 0.008039606 0.6635963
med     0.0132433783 0.418421053 6.9139197
iqr     0.0002498751 0.007894737 0.6255247
min     0.0130767949 0.413157895 6.6721094
max     0.0135765451 0.428947368 7.9231588
invalid 0.0000000000 0.000000000 0.0000000
ps.nb.st <- sapply(getIterationsInfo(nb.st.res), function(i) i$probs[,1])
ts.nb.st <- sapply(getIterationsInfo(nb.st.res), function(i) i$probs[,2])
PRcurve(ps.nb,ts.nb,main="PR curve",lty=1,
        xlim=c(0,1),ylim=c(0,1), avg="vertical")
PRcurve(ps.orh,ts.orh,add=TRUE,lty=1, color='grey', avg='vertical')
PRcurve(ps.nb.st,ts.nb.st,add=TRUE,lty=2,avg='vertical')
legend('topright',c('NaiveBayes','ORh','NaiveBayes-ST'),
       lty=c(1,1,2),col=c('black','grey','black'))


CRchart(ps.nb,ts.nb,main='Cumulative Recall curve',
        lty=1,xlim=c(0,1),ylim=c(0,1),avg='vertical')
CRchart(ps.orh,ts.orh,add=TRUE,lty=1,color='grey',avg='vertical')
CRchart(ps.nb.st,ts.nb.st,add=TRUE,lty=2,avg='vertical')
legend('bottomright',c('NaiveBayes','ORh','NaiveBayes-ST'),
       lty=c(1,1,2),col=c('black','grey','grey'))

par(mfrow=c(1,2)) 
ps.nb.st <- sapply(getIterationsInfo(nb.st.res), function(i) i$probs[,1])
ts.nb.st <- sapply(getIterationsInfo(nb.st.res), function(i) i$probs[,2])
PRcurve(ps.nb,ts.nb,main="PR curve",lty=1,
        xlim=c(0,1),ylim=c(0,1), avg="vertical")
PRcurve(ps.orh,ts.orh,add=T,lty=1, color='grey', avg='vertical')
PRcurve(ps.nb.st,ts.nb.st,add=T,lty=2,avg='vertical')
legend('topright',c('NaiveBayes','ORh','NaiveBayes-ST'),
       lty=c(1,1,2),col=c('black','grey','black'))

CRchart(ps.nb,ts.nb,main='Cumulative Recall curve',
        lty=1,xlim=c(0,1),ylim=c(0,1),avg='vertical')
CRchart(ps.orh,ts.orh,add=T,lty=1,color='grey',avg='vertical')
CRchart(ps.nb.st,ts.nb.st,add=T,lty=2,avg='vertical')
legend('bottomright',c('NaiveBayes','ORh','NaiveBayes-ST'),
       lty=c(1,1,2),col=c('black','grey','grey'))

LS0tCnRpdGxlOiAiRGV0ZWN0aW5nIEZyYXVkdWxlbnQgVHJhbnNhY3Rpb25zIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgoKIyMjIEdvYWwgb2YgdGhpcyBzdHVkeQoKV2UgYXJlIGdpdmVuIGEgZGF0YXNldCB3aXRoIHRyYW5zYWN0aW9ucyBvZiBhIHNldCBvZiBwcm9kdWN0cyB0aGF0IGFyZSByZXBvcnRlZCBieSB0aGUgc2FsZXNwZW9wbGUgb2Ygc29tZSBjb21wYW55LiBPdXIgZ29hbCBpcyB0byBmaW5kICJzdHJhbmdlIiB0cmFuc2FjdGlvbnMgKGFrYSBvdXRsaWVycykgdGhhdCBtYXkgaW5kaWNhdGUgZnJhdWQgYXR0ZW1wdHMgYnkgc29tZSBvZiB0aGUgc2FsZXNwZW9wbGUuIAoKIyMjIyBIb3cgdG8gZGV0ZXJtaW5lIGlmIGEgdHJhbnNhY3Rpb24gaXMgZnJhdWR1bGFudApTb21lIHNpZ25zIHRvIGxvb2sgb3V0IGZvciB3aGVuIGRldGVybWluaW5nIHdoZXRoZXIgYSB0cmFuc2FjdGlvbiBpcyBmcmF1ZHVsZW50OiAgCgpBIGNoYW5nZSBpbiBmcmVxdWVuY3kgb2Ygb3JkZXJzIHBsYWNlZCwgZm9yIGV4YW1wbGUsIGEgY3VzdG9tZXIgd2hvIHR5cGljYWxseSBwbGFjZXMgYSBjb3VwbGUgb2Ygb3JkZXJzIGEgbW9udGgsIHN1ZGRlbmx5IG1ha2VzIG51bWVyb3VzIHRyYW5zYWN0aW9ucyB3aXRoaW4gYSBzaG9ydCBzcGFuIG9mIHRpbWUsIHNvbWV0aW1lcyB3aXRoaW4gbWludXRlcyBvZiB0aGUgcHJldmlvdXMgb3JkZXIuIFwgCk9yZGVycyB0aGF0IGFyZSBzaWduaWZpY2FudGx5IGhpZ2hlciB0aGFuIGEgdXNlcuKAmXMgYXZlcmFnZSB0cmFuc2FjdGlvbi4gXCAKQnVsayBvcmRlcnMgb2YgdGhlIHNhbWUgaXRlbSB3aXRoIHNsaWdodCB2YXJpYXRpb25zIHN1Y2ggYXMgY29sb3Igb3Igc2l6ZeKAlGVzcGVjaWFsbHkgaWYgdGhpcyBpcyBhdHlwaWNhbCBvZiB0aGUgdXNlcuKAmXMgdHJhbnNhY3Rpb24gaGlzdG9yeS4gXApBIHN1ZGRlbiBjaGFuZ2UgaW4gZGVsaXZlcnkgcHJlZmVyZW5jZSwgZm9yIGV4YW1wbGUsIGEgY2hhbmdlIGZyb20gaG9tZSBvciBvZmZpY2UgZGVsaXZlcnkgYWRkcmVzcyB0byBpbi1zdG9yZSwgd2FyZWhvdXNlLCBvciBQTyBCb3ggZGVsaXZlcnkuIFwKQSBtaXNtYXRjaGVkIElQIEFkZHJlc3MsIG9yIGFuIElQIEFkZHJlc3MgdGhhdCBpcyBub3QgZnJvbSB0aGUgZ2VuZXJhbCBsb2NhdGlvbiBvciBhcmVhIG9mIHRoZSBiaWxsaW5nIGFkZHJlc3MuCgojIyMgVGhlIEF2YWlsYWJsZSBEYXRhCgpUaGUgZGF0YSBjb250YWlucyA0MDEsMTQ2IHJvd3MgYW5kIGluY2x1ZGVzIGluZm9ybWF0aW9uIG9uIG9uZSBzYWxlIGJ5IHRoZSBzYWxlc3BlcnNvbi4gV2UgaGF2ZSB0aGUgZm9sbG93aW5nIGNvbHVtbnM6IFwKKiBJRCAtIElEIG9mIHRoZSBzYWxlc3BlcnNvbiBcIAoqIFByb2QgLSBJRCBvZiB0aGUgc29sZCBwcm9kdWN0IFwKKiBRdWFudCAtIG51bWJlciBvZiByZXBvcnRlZCBzb2xkIHVuaXRzIG9mIHRoZSBwcm9kdWN0IFwKKiBWYWwgLSB0aGUgcmVwb3J0ZWQgdG90YWwgbW9uZXRhcnkgdmFsdWUgb2YgdGhlIHNhbGUgXAoqIEluc3AgLSAqKm9rOioqIGlmIHRyYW5zYWN0aW9uIGlzIG5vdCBmcmF1ZHVsYW50LCAqKmZyYXVkKiogaWYgaXQgaXMsICoqdW5rbioqIGlmIHVua25vd24KCiMjIyMgSW1wb3J0aW5nIHRoZSBEYXRhCgpgYGB7cn0KbGlicmFyeShkcGx5cikKZGF0YShzYWxlcywgcGFja2FnZT0iRE13UjIiKQpgYGAKYGBge3J9CnNhbGVzCmBgYAoKIyMjIyMgRXhwbG9yaW5nIHRoZSBEYXRhc2V0CgojIyMjIyMgRmluZGluZyBOdW1iZXIgb2YgdW5pcXVlIHZhbHVlcyBpbiB0aGUgZGF0YXNldApgYGB7cn0KbGVuZ3RoKHVuaXF1ZSh1bmxpc3Qoc2FsZXNbYygiSUQiKV0pKSkKYGBgCmBgYHtyfQpsZW5ndGgodW5pcXVlKHVubGlzdChzYWxlc1tjKCJQcm9kIildKSkpCmBgYApgYGB7cn0KbGVuZ3RoKHVuaXF1ZSh1bmxpc3Qoc2FsZXNbYygiSW5zcCIpXSkpKQpgYGAKCmBgYHtyfQpzdW1tYXJ5KHNhbGVzKQpgYGAKClRvIGNoZWNrIGlmIHdlIGhhdmUgYSBzaWduaWZpY2FudCBudW1iZXIgb2YgcHJvZHVjdHMgYW5kIHNhbGVzcGVvcGxlIHdlIHVzZSB0aGUgbmxldmVscyBtZXRob2QgKEl0IHJldHVybiB0aGUgbnVtYmVyIG9mIGxldmVscyB3aGljaCBpdHMgYXJndW1lbnQgaGFzICkKCmBgYHtyfQpubGV2ZWxzKHNhbGVzJElEKQpubGV2ZWxzKHNhbGVzJFByb2QpCmBgYAoKTG9va2luZyBhdCB0aGUgZGF0YSB3aGVyZSB3ZSBoYXZlIG1pc3NpbmcgdmFsdWVzIGZvciBRdWFudGl0eSBhbmQgVmFsdWUKYGBge3J9CmZpbHRlcihzYWxlcyxpcy5uYShRdWFudCksaXMubmEoVmFsKSkKYGBgCgpXZSBjYW4gbG9vayBhdCB0aGUgcHJvcG9ydGlvbnMgb2YgdGhlIG1pc3NpbmcgdmFsdWVzCmBgYHtyfQp0YWJsZShzYWxlcyRJbnNwKS9ucm93KHNhbGVzKSAqIDEwMApgYGAKCmBgYHtyfQpsaWJyYXJ5KGdncGxvdDIpCmdncGxvdChncm91cF9ieShzYWxlcyxJRCkgJT4lIHN1bW1hcml6ZShuVHJhbnM9bigpKSxhZXMoeD1JRCx5PW5UcmFucykpICsKICAgIGdlb21fYmFyKHN0YXQ9ImlkZW50aXR5IikgKyAKICAgIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLCBheGlzLnRpY2tzLng9ZWxlbWVudF9ibGFuaygpKSArIAogICAgeGxhYigiU2FsZXNtZW4iKSArIHlsYWIoIk5yLiBvZiBUcmFuc2FjdGlvbnMiKSArCiAgICBnZ3RpdGxlKCJOci4gb2YgVHJhbnNhY3Rpb25zIHBlciBTYWxlc21hbiIpCmdncGxvdChncm91cF9ieShzYWxlcyxQcm9kKSAlPiUgc3VtbWFyaXplKG5UcmFucz1uKCkpLGFlcyh4PVByb2QseT1uVHJhbnMpKSArCiAgICBnZW9tX2JhcihzdGF0PSJpZGVudGl0eSIpICsgCiAgICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50aWNrcy54PWVsZW1lbnRfYmxhbmsoKSkgKyAKICAgIHhsYWIoIlByb2R1Y3QiKSArIHlsYWIoIk5yLiBvZiBUcmFuc2FjdGlvbnMiKSArCiAgICBnZ3RpdGxlKCJOci4gb2YgVHJhbnNhY3Rpb25zIHBlciBQcm9kdWN0IikKYGBgCmBgYHtyfQpnZ3Bsb3QoZ3JvdXBfYnkoc2FsZXMsSUQpICU+JSBzdW1tYXJpemUoblRyYW5zPW4oKSksYWVzKHg9SUQseT1uVHJhbnMpKSArCiAgICBnZW9tX2JhcihzdGF0PSJpZGVudGl0eSIpICsgCiAgICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50aWNrcy54PWVsZW1lbnRfYmxhbmsoKSkgKyAKICAgIHhsYWIoIlByb2R1Y3QiKSArIHlsYWIoIk5yLiBvZiBUcmFuc2FjdGlvbnMiKSArCiAgICBnZ3RpdGxlKCJOci4gb2YgVHJhbnNhY3Rpb25zIHBlciBTYWxlc21hbiIpCmBgYApgYGB7cn0KZ2dwbG90KGdyb3VwX2J5KHNhbGVzLFByb2QpICU+JSBzdW1tYXJpemUoblRyYW5zPW4oKSksYWVzKHg9UHJvZCx5PW5UcmFucykpICsKICAgIGdlb21fYmFyKHN0YXQ9ImlkZW50aXR5IikgKyAKICAgIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLCBheGlzLnRpY2tzLng9ZWxlbWVudF9ibGFuaygpKSArIAogICAgeGxhYigiU2FsZXNtZW4iKSArIHlsYWIoIk5yLiBvZiBUcmFuc2FjdGlvbnMiKSArCiAgICBnZ3RpdGxlKCJOci4gb2YgVHJhbnNhY3Rpb25zIHBlciBQcm9kdWN0IikKYGBgCgpOb3csIHdlIHdpbGwgYWRkIHRoZSB1bml0IHByaWNlIHRvIHRoZSBkYXRhIHNldApgYGB7cn0Kc2FsZXMgPC0gbXV0YXRlKHNhbGVzLFVwcmljZT1WYWwvUXVhbnQpCmBgYAoKYGBge3J9CnNhbGVzCmBgYApUaGUgdmFyaWFiaWxpdHkgc2hvdWxkIGJlIGxvd2VyIGZvciB1bml0IHByaWNlCgpgYGB7cn0Kc3VtbWFyeShzYWxlcyRVcHJpY2UpCmBgYAoKU2VlaW5nIHRoZSBkYXRhIGZvciBsb3dlc3QgbWVkaWFuIHVuaXQgcHJpY2UgYW5kIGhpZ2hlc3QgbWVkaWFuIHVuaXQgcHJpY2UuCmBgYHtyfQpwcm9kcyA8LSBncm91cF9ieShzYWxlcyxQcm9kKQptcFByb2RzIDwtIHN1bW1hcml6ZShwcm9kcyxtZWRpYW5QcmljZT1tZWRpYW4oVXByaWNlLG5hLnJtPVRSVUUpKQpiaW5kX2NvbHMobXBQcm9kcyAlPiUgYXJyYW5nZShtZWRpYW5QcmljZSkgJT4lIHNsaWNlKDE6NSksCiAgICAgICAgICBtcFByb2RzICU+JSBhcnJhbmdlKGRlc2MobWVkaWFuUHJpY2UpKSAlPiUgc2xpY2UoMTo1KSkKYGBgCldlIHVzaW5nIHRoZSBsb2cgc2NhbGUgYmVjYXVzZSB0aGUgc2NhbGVzIG9mIHByaWNlIG9mIHRoZSBtb3N0IGV4cGVuc2l2ZSBhbmQgdGhlIGxlYXN0IGV4cGVuc2l2ZSBwcm9kdWN0cyBhcmUgZGlmZmVyZW50LiAKClRoaXMgc2hvd3MgdGhlIGRpc3RyaWJ1dGlvbiBvZiB0aGUgdW5pdCBwcmljZXMgb2YgdGhlIGNoZWFwZXN0IGFuZCB0aGUgbW9zdCBleHBlbnNpdmUgcHJvZHVjdHMuCmBgYHtyfQpsaWJyYXJ5KGdncGxvdDIpCmxpYnJhcnkoZm9yY2F0cykKZ2dwbG90KGZpbHRlcihzYWxlcyxQcm9kICVpbiUgYygicDM2ODkiLCJwNTYwIikpLGFlcyh4PWZjdF9kcm9wKFByb2QpLHk9VXByaWNlKSkgKwogICAgZ2VvbV9ib3hwbG90KCkgKyBzY2FsZV95X2xvZzEwKCkgKyAKICAgIHhsYWIoIiIpICsgeWxhYigibG9nMTAoVW5pdFByaWNlKSIpCmBgYApTaW1pbGFybHkgd2Ugd2lsbCBkbyB0aGlzIGZvciB0aGUgdG90YWwgdmFsdWUgb2YgdGhlIHNhbGUKCmBgYHtyfQppZHMgPC0gZ3JvdXBfYnkoc2FsZXMsSUQpCnR2SURzIDwtIHN1bW1hcml6ZShpZHMsdG90YWxWYWw9c3VtKFZhbCxuYS5ybT1UUlVFKSkKYmluZF9jb2xzKHR2SURzICU+JSBhcnJhbmdlKHRvdGFsVmFsKSAlPiUgc2xpY2UoMTo1KSwKICAgICAgICAgIHR2SURzICU+JSBhcnJhbmdlKGRlc2ModG90YWxWYWwpKSAlPiUgc2xpY2UoMTo1KSkKYGBgCgpgYGB7cn0KYXJyYW5nZSh0dklEcyxkZXNjKHRvdGFsVmFsKSkgJT4lIHNsaWNlKDE6MTAwKSAlPiUgCiAgICBzdW1tYXJpemUodDEwMD1zdW0odG90YWxWYWwpKSAvIAogICAgKHN1bW1hcml6ZSh0dklEcyxzdW0odG90YWxWYWwpKSkgKiAxMDAKYXJyYW5nZSh0dklEcyx0b3RhbFZhbCkgJT4lIHNsaWNlKDE6MjAwMCkgJT4lIAogICAgc3VtbWFyaXplKGIyMDAwPXN1bSh0b3RhbFZhbCkpIC8gCiAgICAoc3VtbWFyaXplKHR2SURzLHN1bSh0b3RhbFZhbCkpKSAqIDEwMApgYGAKCk5vdywgd2UnbGwgbG9vayBhdCB0aGUgc2FtZSBhbmFseXNpcyBmb3IgcXVhbnRpdHkKCmBgYHtyfQpwcm9kcyA8LSBncm91cF9ieShzYWxlcyxQcm9kKQpxdFByb2RzIDwtIHN1bW1hcml6ZShwcm9kcyx0b3RhbFF0eT1zdW0oUXVhbnQsbmEucm09VFJVRSkpCmJpbmRfY29scyhxdFByb2RzICU+JSBhcnJhbmdlKGRlc2ModG90YWxRdHkpKSAlPiUgc2xpY2UoMTo1KSwKICAgICAgICAgIHF0UHJvZHMgJT4lIGFycmFuZ2UodG90YWxRdHkpICU+JSBzbGljZSgxOjUpKQpgYGAKCmBgYHtyfQphcnJhbmdlKHF0UHJvZHMsZGVzYyh0b3RhbFF0eSkpICU+JSBzbGljZSgxOjEwMCkgJT4lIAogICAgc3VtbWFyaXplKHQxMDA9c3VtKGFzLm51bWVyaWModG90YWxRdHkpKSkgLyAKICAgIChzdW1tYXJpemUocXRQcm9kcyxzdW0oYXMubnVtZXJpYyh0b3RhbFF0eSkpKSkgKiAxMDAKYXJyYW5nZShxdFByb2RzLHRvdGFsUXR5KSAlPiUgc2xpY2UoMTo0MDAwKSAlPiUgCiAgICBzdW1tYXJpemUoYjQwMDA9c3VtKGFzLm51bWVyaWModG90YWxRdHkpKSkgLyAKICAgIChzdW1tYXJpemUocXRQcm9kcyxzdW0oYXMubnVtZXJpYyh0b3RhbFF0eSkpKSkgKiAxMDAKYGBgCgpgYGB7cn0Kbm91dHMgPC0gZnVuY3Rpb24oeCkgbGVuZ3RoKGJveHBsb3Quc3RhdHMoeCkkb3V0KQpub3V0c1Byb2RzIDwtIHN1bW1hcmlzZShwcm9kcyxuT3V0PW5vdXRzKFVwcmljZSkpCmBgYAoKVGhpcyByZXN1bHQgYmVsb3cgaXMgc2hvd2luZyB0aGUgbnVtYmVyIG9mICJzdHJhbmdlIiB0cmFuc2FjdGlvbnMgKG91dGxpZXJzKSBmb3IgZWFjaCBwcm9kdWN0CmBgYHtyfQphcnJhbmdlKG5vdXRzUHJvZHMsZGVzYyhuT3V0KSkKYGBgCgpUb3RhbCBudW1iZXIgb2Ygb3V0bGllcnMKYGBge3J9CnN1bW1hcml6ZShub3V0c1Byb2RzLHRvdGFsT3V0cz1zdW0obk91dCkpCnN1bW1hcml6ZShub3V0c1Byb2RzLHRvdGFsT3V0cz1zdW0obk91dCkpL25yb3coc2FsZXMpKjEwMApgYGAKCiMjIyMgRGF0YSBQcm9ibGVtcwoKIyMjIyBNaXNzaW5nIHZhbHVlcyBvZiB0aGUgZGF0YSBzZXQKClRoaXMgcmV0dXJucyB0aGUgc2FsZXNwZW9wbGUgd2l0aCBhIGxhcmdlciBwcm9wb3J0aW9uIG9mIHRyYW5zYWN0aW9ucyBvZiB1bmtub3ducyBvbiBib3RoIFZhbCBhbmQgUXVhbnRpdHkKYGBge3J9CnByb3AubmFRYW5kViA8LSBmdW5jdGlvbihxLHYpIDEwMCpzdW0oaXMubmEocSkgJiBpcy5uYSh2KSkvbGVuZ3RoKHEpCnN1bW1hcmlzZShpZHMsblByb2JzPXByb3AubmFRYW5kVihRdWFudCxWYWwpKSAlPiUgYXJyYW5nZShkZXNjKG5Qcm9icykpCmBgYAoKU2ltaWxhcmx5LCBwcm9kdWN0cyB3aXRoIGhpZ2hlc3QgcHJvcG9ydGlvbnMgb2YgbWlzc2luZyB2YWx1ZXMKYGBge3J9CnN1bW1hcmlzZShwcm9kcyxuUHJvYnM9cHJvcC5uYVFhbmRWKFF1YW50LFZhbCkpICU+JSBhcnJhbmdlKGRlc2MoblByb2JzKSkKYGBgCnJlbW92aW5nIHRyYW5zYWN0aW9ucyB3aXRoIHVua25vd24gdmFsdWVzIGZvciBRdWFudGl0eSBhbmQgVmFsdWUKYGBge3J9CnNhbGVzIDwtIGZpbHRlcihzYWxlcywhKGlzLm5hKFF1YW50KSAmIGlzLm5hKFZhbCkpKQpgYGAKCmBgYHtyfQpwcm9wLm5hcyA8LSBmdW5jdGlvbih4KSAxMDAqc3VtKGlzLm5hKHgpKS9sZW5ndGgoeCkKc3VtbWFyaXNlKHByb2RzLHByb3BOQS5RPXByb3AubmFzKFF1YW50KSkgJT4lIGFycmFuZ2UoZGVzYyhwcm9wTkEuUSkpCmBgYAoKYGBge3J9CmZpbHRlcihzYWxlcywgUHJvZCAlaW4lIGMoInAyNDQyIiwicDI0NDMiKSkgJT4lIAogICAgZ3JvdXBfYnkoSW5zcCkgJT4lIGNvdW50KCkKYGBgCgpgYGB7cn0Kc2FsZXMgPC0gZHJvcGxldmVscyhmaWx0ZXIoc2FsZXMsIShQcm9kICVpbiUgYygicDI0NDIiLCAicDI0NDMiKSkpKQpgYGAKCmBgYHtyfQpzdW1tYXJpc2UoaWRzLHByb3BOQS5RPXByb3AubmFzKFF1YW50KSkgJT4lIGFycmFuZ2UoZGVzYyhwcm9wTkEuUSkpCmBgYAoKYGBge3J9CnN1bW1hcmlzZShwcm9kcyxwcm9wTkEuVj1wcm9wLm5hcyhWYWwpKSAlPiUgYXJyYW5nZShkZXNjKHByb3BOQS5WKSkKYGBgCgpgYGB7cn0Kc3VtbWFyaXNlKGlkcyxwcm9wTkEuVj1wcm9wLm5hcyhWYWwpKSAlPiUgYXJyYW5nZShkZXNjKHByb3BOQS5WKSkKYGBgCgpgYGB7cn0KdFByaWNlIDwtIGZpbHRlcihzYWxlcywgSW5zcCAhPSAiZnJhdWQiKSAlPiUgCiAgICAgICAgICBncm91cF9ieShQcm9kKSAlPiUgCiAgICAgICAgICBzdW1tYXJpc2UobWVkaWFuUHJpY2UgPSBtZWRpYW4oVXByaWNlLG5hLnJtPVRSVUUpKQpgYGAKCmBgYHtyfQpub1F1YW50TWVkUHJpY2VzIDwtIGZpbHRlcihzYWxlcywgaXMubmEoUXVhbnQpKSAlPiUgCiAgICBpbm5lcl9qb2luKHRQcmljZSkgJT4lIAogICAgc2VsZWN0KG1lZGlhblByaWNlKQpub1ZhbE1lZFByaWNlcyA8LSBmaWx0ZXIoc2FsZXMsIGlzLm5hKFZhbCkpICU+JSAKICAgIGlubmVyX2pvaW4odFByaWNlKSAlPiUgCiAgICBzZWxlY3QobWVkaWFuUHJpY2UpCgpub1F1YW50IDwtIHdoaWNoKGlzLm5hKHNhbGVzJFF1YW50KSkKbm9WYWwgPC0gd2hpY2goaXMubmEoc2FsZXMkVmFsKSkKc2FsZXNbbm9RdWFudCwnUXVhbnQnXSA8LSBjZWlsaW5nKHNhbGVzW25vUXVhbnQsJ1ZhbCddIC9ub1F1YW50TWVkUHJpY2VzKQpzYWxlc1tub1ZhbCwnVmFsJ10gPC0gc2FsZXNbbm9WYWwsJ1F1YW50J10gKiBub1ZhbE1lZFByaWNlcwpgYGAKCmBgYHtyfQpzYWxlcyRVcHJpY2UgPC0gc2FsZXMkVmFsL3NhbGVzJFF1YW50CmBgYAoKYGBge3J9CnNhdmUoc2FsZXMsIGZpbGUgPSAic2FsZXNDbGVhbi5SZGF0YSIpCmBgYAoKYGBge3J9Cm1zIDwtIGZpbHRlcihzYWxlcyxJbnNwICE9ICJmcmF1ZCIpICU+JSAKICAgIGdyb3VwX2J5KFByb2QpICU+JSAKICAgIHN1bW1hcml6ZShtZWRpYW49bWVkaWFuKFVwcmljZSxuYS5ybT1UUlVFKSwKICAgICAgICAgICAgICBpcXI9SVFSKFVwcmljZSxuYS5ybT1UUlVFKSwKICAgICAgICAgICAgICBuVHJhbnM9bigpLAogICAgICAgICAgICAgIGZld1RyYW5zPWlmZWxzZShuVHJhbnM+MjAsRkFMU0UsVFJVRSkpCm1zCmBgYAoKUHJvcGVydGllcyBvZiB0aGUgZGlzdHJpYnV0aW9uIG9mIHVuaXQgcHJpY2VzCmBgYHtyfQpnZ3Bsb3QobXMsYWVzKHg9bWVkaWFuLHk9aXFyLGNvbG9yPWZld1RyYW5zKSkgKyAKICAgIGdlb21fcG9pbnQoKSArIAogICAgeGxhYigiTWVkaWFuIikgKyB5bGFiKCJJUVIiKQpnZ3Bsb3QobXMsYWVzKHg9bWVkaWFuLHk9aXFyLGNvbG9yPWZld1RyYW5zKSkgKyAKICAgIGdlb21fcG9pbnQoKSArIAogICAgc2NhbGVfeV9sb2cxMCgpICsgc2NhbGVfeF9sb2cxMCgpICsgCiAgICB4bGFiKCJsb2coTWVkaWFuKSIpICsgeWxhYigibG9nKElRUikiKQpgYGAKCmBgYHtyfQptcyA8LSBtdXRhdGUobXMsc21lZGlhbj1zY2FsZShtZWRpYW4pLHNpcXI9c2NhbGUoaXFyKSkKc21hbGxzIDwtIHdoaWNoKG1zJGZld1RyYW5zKQpuc21hbGxzIDwtIGFzLmNoYXJhY3RlcihtcyRQcm9kW3NtYWxsc10pCnNpbWlsYXIgPC0gbWF0cml4KE5BLGxlbmd0aChzbWFsbHMpLDcsCiAgICBkaW1uYW1lcz1saXN0KG5zbWFsbHMsCiAgICAgIGMoIlJvd1NpbVByb2QiLCAia3Muc3RhdCIsICJrcy5wIiwgIm1lZFAiLCAiaXFyUCIsICJtZWRTIiwiaXFyUyIpKSkKeHByb2RzIDwtIHRhcHBseShzYWxlcyRVcHJpY2UsIHNhbGVzJFByb2QsIGxpc3QpCmZvcihpIGluIHNlcV9hbG9uZyhzbWFsbHMpKSB7CiAgICBkIDwtIHNjYWxlKG1zWyxjKCJzbWVkaWFuIiwic2lxciIpXSwKICAgICAgICAgICAgICAgYyhtcyRzbWVkaWFuW3NtYWxsc1tpXV0sbXMkc2lxcltzbWFsbHNbaV1dKSwKICAgICAgICAgICAgICAgRkFMU0UpCiAgICBkIDwtIHNxcnQoZHJvcChkXjIgJSolIHJlcCgxLCBuY29sKGQpKSkpCiAgICBzdGF0IDwtIGtzLnRlc3QoeHByb2RzW1tuc21hbGxzW2ldXV0sIHhwcm9kc1tbb3JkZXIoZClbMl1dXSkKICAgIHNpbWlsYXJbaSwgXSA8LSBjKG9yZGVyKGQpWzJdLCBzdGF0JHN0YXRpc3RpYywgc3RhdCRwLnZhbHVlLAogICAgICAgICAgICAgICAgICAgICAgbXMkbWVkaWFuW3NtYWxsc1tpXV0sbXMkaXFyW3NtYWxsc1tpXV0sCiAgICAgICAgICAgICAgICAgICAgICBtcyRtZWRpYW5bb3JkZXIoZClbMl1dLG1zJGlxcltvcmRlcihkKVsyXV0pCn0KYGBgCgpgYGB7cn0KaGVhZChzaW1pbGFyKQpgYGAKCmBgYHtyfQpiaW5kX3Jvd3MoZmlsdGVyKG1zLFByb2Q9PXJvd25hbWVzKHNpbWlsYXIpWzFdKSwKICAgICAgICAgIG1zW3NpbWlsYXJbMSwxXSxdKQpgYGAKYGBge3J9Cm5yb3coc2ltaWxhcltzaW1pbGFyWywgImtzLnAiXSA+PSAwLjksIF0pCmBgYAoKYGBge3J9CnN1bShzaW1pbGFyWywgImtzLnAiXSA+PSAwLjkpCmBgYAoKYGBge3J9CnNhdmUoc2ltaWxhciwgZmlsZSA9ICJzaW1pbGFyUHJvZHVjdHMuUmRhdGEiKQpgYGAKCiMjIyBEZWZpbmluZyB0aGUgRGF0YSBNaW5pbmcgVGFza3MKCiMjIyMgRXZhbHVhdGlvbiBDcml0ZXJpYQoKYGBge3J9CmxpYnJhcnkoUk9DUikKZGF0YShST0NSLnNpbXBsZSkKcHJlZCA8LSBwcmVkaWN0aW9uKFJPQ1Iuc2ltcGxlJHByZWRpY3Rpb25zLCBST0NSLnNpbXBsZSRsYWJlbHMpCnBlcmYgPC0gcGVyZm9ybWFuY2UocHJlZCwgInByZWMiLCAicmVjIikKcGxvdChwZXJmKQpgYGAKCmBgYHtyfQpQUmN1cnZlIDwtIGZ1bmN0aW9uKHByZWRzLCB0cnVlcywgLi4uKSB7CiAgICByZXF1aXJlKFJPQ1IsIHF1aWV0bHkgPSBUUlVFKQogICAgcGQgPC0gcHJlZGljdGlvbihwcmVkcywgdHJ1ZXMpCiAgICBwZiA8LSBwZXJmb3JtYW5jZShwZCwgInByZWMiLCAicmVjIikKICAgIHBmQHkudmFsdWVzIDwtIGxhcHBseShwZkB5LnZhbHVlcywgZnVuY3Rpb24oeCkgcmV2KGN1bW1heChyZXYoeCkpKSkKICAgIHBsb3QocGYsIC4uLikKfQpgYGAKCmBgYHtyfQpQUmN1cnZlKFJPQ1Iuc2ltcGxlJHByZWRpY3Rpb25zLCBST0NSLnNpbXBsZSRsYWJlbHMpCmBgYAoKYGBge3J9CmxpYnJhcnkoUk9DUikKZGF0YShST0NSLnNpbXBsZSkKcHJlZCA8LSBwcmVkaWN0aW9uKFJPQ1Iuc2ltcGxlJHByZWRpY3Rpb25zLCBST0NSLnNpbXBsZSRsYWJlbHMpCnBlcmYgPC0gcGVyZm9ybWFuY2UocHJlZCwgInByZWMiLCAicmVjIikKcGFyKCBtZnJvdz1jKDEsMikgKQpwbG90KHBlcmYpClBSY3VydmUoUk9DUi5zaW1wbGUkcHJlZGljdGlvbnMsIFJPQ1Iuc2ltcGxlJGxhYmVscykKcGFyKCBtZnJvdz1jKDEsMSkgKQpgYGAKCmBgYHtyfQpwcmVkIDwtIHByZWRpY3Rpb24oUk9DUi5zaW1wbGUkcHJlZGljdGlvbnMsIFJPQ1Iuc2ltcGxlJGxhYmVscykKcGVyZiA8LSBwZXJmb3JtYW5jZShwcmVkLCAibGlmdCIsICJycHAiKQpwbG90KHBlcmYsIG1haW4gPSAiTGlmdCBDaGFydCIpCmBgYAoKYGBge3J9CkNSY2hhcnQgPC0gZnVuY3Rpb24ocHJlZHMsIHRydWVzLCAuLi4pIHsKICAgIHJlcXVpcmUoUk9DUiwgcXVpZXRseSA9IFQpCiAgICBwZCA8LSBwcmVkaWN0aW9uKHByZWRzLCB0cnVlcykKICAgIHBmIDwtIHBlcmZvcm1hbmNlKHBkLCAicmVjIiwgInJwcCIpCiAgICBwbG90KHBmLCAuLi4pCn0KYGBgCgpgYGB7cn0KQ1JjaGFydChST0NSLnNpbXBsZSRwcmVkaWN0aW9ucywgUk9DUi5zaW1wbGUkbGFiZWxzLCAKICAgICAgICBtYWluPSdDdW11bGF0aXZlIFJlY2FsbCBDaGFydCcpCmBgYAoKYGBge3J9CmF2Z05EVFAgPC0gZnVuY3Rpb24odG9JbnNwLHRyYWluLHN0YXRzKSB7CiAgaWYgKG1pc3NpbmcodHJhaW4pICYmIG1pc3Npbmcoc3RhdHMpKSAKICAgICAgc3RvcCgnUHJvdmlkZSBlaXRoZXIgdGhlIHRyYWluaW5nIGRhdGEgb3IgdGhlIHByb2R1Y3Qgc3RhdHMnKQogIGlmIChtaXNzaW5nKHN0YXRzKSkgewogICAgICBzdGF0cyA8LSBhcy5tYXRyaXgoZmlsdGVyKHRyYWluLEluc3AgIT0gJ2ZyYXVkJykgJT4lCiAgICAgICAgICAgICAgICAgICAgICAgICBncm91cF9ieShQcm9kKSAlPiUKICAgICAgICAgICAgICAgICAgICAgICAgIHN1bW1hcmlzZShtZWRpYW49bWVkaWFuKFVwcmljZSksaXFyPUlRUihVcHJpY2UpKSAlPiUKICAgICAgICAgICAgICAgICAgICAgICAgIHNlbGVjdChtZWRpYW4saXFyKSkKICAgICAgcm93bmFtZXMoc3RhdHMpIDwtIGxldmVscyh0cmFpbiRQcm9kKQogICAgICBzdGF0c1t3aGljaChzdGF0c1ssJ2lxciddPT0wKSwnaXFyJ10gPC0gc3RhdHNbd2hpY2goc3RhdHNbLCdpcXInXT09MCksJ21lZGlhbiddCiAgfQogIAogIHJldHVybihtZWFuKGFicyh0b0luc3AkVXByaWNlLXN0YXRzW3RvSW5zcCRQcm9kLCdtZWRpYW4nXSkgLwogICAgICAgICAgICAgICAgIHN0YXRzW3RvSW5zcCRQcm9kLCdpcXInXSkpCn0KCmBgYAoKCiMjIyMjIEV4cGVyaW1lbnRhbCBNZXRob2RvbG9neQoKYGBge3J9CmV2YWxPdXRsaWVyUmFua2luZyA8LSBmdW5jdGlvbih0ZXN0U2V0LHJhbmtPcmRlcixUaHJlc2hvbGQsc3RhdHNQcm9kcywuLi4pIAp7CiAgIG9yZFRTIDwtIHRlc3RTZXRbcmFua09yZGVyLF0KICAgTiA8LSBucm93KHRlc3RTZXQpCiAgIG5GIDwtIGlmIChUaHJlc2hvbGQgPCAxKSBhcy5pbnRlZ2VyKFRocmVzaG9sZCpOKSBlbHNlIFRocmVzaG9sZAogICBjbSA8LSB0YWJsZShjKHJlcCgnZnJhdWQnLG5GKSxyZXAoJ29rJyxOLW5GKSksb3JkVFMkSW5zcCkKICAgcHJlYyA8LSBjbVsnZnJhdWQnLCdmcmF1ZCddL3N1bShjbVsnZnJhdWQnLF0pCiAgIHJlYyA8LSBjbVsnZnJhdWQnLCdmcmF1ZCddL3N1bShjbVssJ2ZyYXVkJ10pCiAgIEFWR25kdHAgPC0gYXZnTkRUUChvcmRUU1sxOm5GLF0sc3RhdHM9c3RhdHNQcm9kcykKICAgcmV0dXJuKGMoUHJlY2lzaW9uPXByZWMsUmVjYWxsPXJlYyxhdmdORFRQPUFWR25kdHApKQp9CmBgYAoKIyMjIE9idGFpbmluZyBPdXRsaWVyIFJhbmtpbmdzCgojIyMjIFVuc3VwZXJ2aXNlZCBBcHByb2FjaGVzCgpgYGB7cn0KQlBydWxlLndmIDwtIGZ1bmN0aW9uKGZvcm0sdHJhaW4sdGVzdCwuLi4pIHsKICAgIHJlcXVpcmUoZHBseXIsIHF1aWV0bHk9VFJVRSkKICAgIG1zIDwtIGFzLm1hdHJpeChmaWx0ZXIodHJhaW4sSW5zcCAhPSAnZnJhdWQnKSAlPiUKICAgICAgICAgICAgICAgICAgICBncm91cF9ieShQcm9kKSAlPiUKICAgICAgICAgICAgICAgICAgICBzdW1tYXJpc2UobWVkaWFuPW1lZGlhbihVcHJpY2UpLGlxcj1JUVIoVXByaWNlKSkgJT4lCiAgICAgICAgICAgICAgICAgICAgc2VsZWN0KG1lZGlhbixpcXIpKQogICAgcm93bmFtZXMobXMpIDwtIGxldmVscyh0cmFpbiRQcm9kKQogICAgbXNbd2hpY2gobXNbLCdpcXInXT09MCksJ2lxciddIDwtIG1zW3doaWNoKG1zWywnaXFyJ109PTApLCdtZWRpYW4nXQogICAgT1JzY29yZSA8LSBhYnModGVzdCRVcHJpY2UtbXNbdGVzdCRQcm9kLCdtZWRpYW4nXSkgLwogICAgICAgICAgICAgICBtc1t0ZXN0JFByb2QsJ2lxciddCiAgICByYW5rT3JkZXIgPC0gb3JkZXIoT1JzY29yZSxkZWNyZWFzaW5nPVRSVUUpCiAgICByZXMgPC0gbGlzdCh0ZXN0U2V0PXRlc3QscmFua09yZGVyPXJhbmtPcmRlciwKICAgICAgICAgICAgICAgIHByb2JzPW1hdHJpeChjKE9Sc2NvcmUsaWZlbHNlKHRlc3QkSW5zcD09J2ZyYXVkJywxLDApKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuY29sPTIpKQogICAgcmVzCn0KYGBgCgpgYGB7cn0KbGlicmFyeShkcGx5cikKZ2xvYmFsU3RhdHMgPC0gYXMubWF0cml4KGZpbHRlcihzYWxlcyxJbnNwICE9ICdmcmF1ZCcpICU+JQogICAgICAgICAgICAgICAgICAgICAgICAgZ3JvdXBfYnkoUHJvZCkgJT4lCiAgICAgICAgICAgICAgICAgICAgICAgICBzdW1tYXJpc2UobWVkaWFuPW1lZGlhbihVcHJpY2UpLGlxcj1JUVIoVXByaWNlKSkgJT4lCiAgICAgICAgICAgICAgICAgICAgICAgICBzZWxlY3QobWVkaWFuLGlxcikpCnJvd25hbWVzKGdsb2JhbFN0YXRzKSA8LSBsZXZlbHMoc2FsZXMkUHJvZCkKZ2xvYmFsU3RhdHNbd2hpY2goZ2xvYmFsU3RhdHNbLCdpcXInXT09MCksJ2lxciddIDwtIAogICAgZ2xvYmFsU3RhdHNbd2hpY2goZ2xvYmFsU3RhdHNbLCdpcXInXT09MCksJ21lZGlhbiddCmhlYWQoZ2xvYmFsU3RhdHMsMykKCmBgYAoKCmBgYHtyfQpsaWJyYXJ5KHBlcmZvcm1hbmNlRXN0aW1hdGlvbikKYnAucmVzIDwtIHBlcmZvcm1hbmNlRXN0aW1hdGlvbigKICAgIFByZWRUYXNrKEluc3AgfiAuLCBzYWxlcyksCiAgICBXb3JrZmxvdygiQlBydWxlLndmIiksCiAgICBFc3RpbWF0aW9uVGFzayhtZXRyaWNzPWMoIlByZWNpc2lvbiIsIlJlY2FsbCIsImF2Z05EVFAiKSwKICAgICAgICAgICAgICAgICAgIG1ldGhvZD1Ib2xkb3V0KG5SZXBzPTMsIGhsZFN6PTAuMywgc3RyYXQ9VFJVRSksCiAgICAgICAgICAgICAgICAgICBldmFsdWF0b3I9ImV2YWxPdXRsaWVyUmFua2luZyIsCiAgICAgICAgICAgICAgICAgICBldmFsdWF0b3IucGFycz1saXN0KFRocmVzaG9sZD0wLjEsIHN0YXRzUHJvZHM9Z2xvYmFsU3RhdHMpKQopCmBgYAoKYGBge3J9CnN1bW1hcnkoYnAucmVzKQoKYGBgCgpgYGB7cn0KcHMuYnAgPC0gc2FwcGx5KGdldEl0ZXJhdGlvbnNJbmZvKGJwLnJlcyksZnVuY3Rpb24oaSkgaSRwcm9ic1ssMV0pCnRzLmJwIDwtIHNhcHBseShnZXRJdGVyYXRpb25zSW5mbyhicC5yZXMpLGZ1bmN0aW9uKGkpIGkkcHJvYnNbLDJdKQpQUmN1cnZlKHBzLmJwLHRzLmJwLG1haW49IlBSIGN1cnZlIixhdmc9InZlcnRpY2FsIikKQ1JjaGFydChwcy5icCx0cy5icCxtYWluPSdDdW11bGF0aXZlIFJlY2FsbCBjdXJ2ZScsYXZnPSd2ZXJ0aWNhbCcpCmBgYAoKYGBge3J9CnBhcihtZnJvdz1jKDEsMikpIApwcy5icCA8LSBzYXBwbHkoZ2V0SXRlcmF0aW9uc0luZm8oYnAucmVzKSwgZnVuY3Rpb24oaSkgaSRwcm9ic1ssMV0pCnRzLmJwIDwtIHNhcHBseShnZXRJdGVyYXRpb25zSW5mbyhicC5yZXMpLCBmdW5jdGlvbihpKSBpJHByb2JzWywyXSkKUFJjdXJ2ZShwcy5icCwgdHMuYnAsIG1haW49IlBSIGN1cnZlIiwgYXZnPSJ2ZXJ0aWNhbCIpCkNSY2hhcnQocHMuYnAsIHRzLmJwLCBtYWluPSdDdW11bGF0aXZlIFJlY2FsbCBjdXJ2ZScsIGF2Zz0ndmVydGljYWwnKQpgYGAKCgpgYGB7cn0KTE9GLndmIDwtIGZ1bmN0aW9uKGZvcm0sIHRyYWluLCB0ZXN0LCBrLCAuLi4pIHsKICAgIHJlcXVpcmUoRE13UjIsIHF1aWV0bHk9VFJVRSkKICAgIG50ciA8LSBucm93KHRyYWluKQogICAgYWxsIDwtIGFzLmRhdGEuZnJhbWUocmJpbmQodHJhaW4sdGVzdCkpCiAgICBOIDwtIG5yb3coYWxsKQogICAgdXBzIDwtIHNwbGl0KGFsbCRVcHJpY2UsYWxsJFByb2QpCiAgICByIDwtIGxpc3QobGVuZ3RoPXVwcykKICAgIGZvcih1IGluIHNlcShhbG9uZz11cHMpKSAKICAgICAgICByW1t1XV0gPC0gaWYgKE5ST1codXBzW1t1XV0pID4gMykgCiAgICAgICAgICAgICAgICAgICAgICBsb2ZhY3Rvcih1cHNbW3VdXSxtaW4oayxOUk9XKHVwc1tbdV1dKSAlLyUgMikpIAogICAgICAgICAgICAgICAgICBlbHNlIGlmIChOUk9XKHVwc1tbdV1dKSkgcmVwKDAsTlJPVyh1cHNbW3VdXSkpIAogICAgICAgICAgICAgICAgICBlbHNlIE5VTEwKICAgIGFsbCRsb2YgPC0gdmVjdG9yKGxlbmd0aD1OKQogICAgc3BsaXQoYWxsJGxvZixhbGwkUHJvZCkgPC0gcgogICAgYWxsJGxvZlt3aGljaCghKGlzLmluZmluaXRlKGFsbCRsb2YpIHwgaXMubmFuKGFsbCRsb2YpKSldIDwtIAogICAgICAgIFNvZnRNYXgoYWxsJGxvZlt3aGljaCghKGlzLmluZmluaXRlKGFsbCRsb2YpIHwgaXMubmFuKGFsbCRsb2YpKSldKQogICAgCiAgICByZXMgPC0gbGlzdCh0ZXN0U2V0PXRlc3QsCiAgICAgICAgICAgICAgICByYW5rT3JkZXI9b3JkZXIoYWxsWyhudHIrMSk6TiwnbG9mJ10sZGVjcmVhc2luZz1UUlVFKSwKICAgICAgICAgICAgICAgIHByb2JzPWFzLm1hdHJpeChjYmluZChhbGxbKG50cisxKTpOLCdsb2YnXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpZmVsc2UodGVzdCRJbnNwPT0nZnJhdWQnLDEsMCkpKSkKICAgIHJlcwp9CmBgYAoKCmBgYHtyfQpsb2YucmVzIDwtIHBlcmZvcm1hbmNlRXN0aW1hdGlvbigKICAgIFByZWRUYXNrKEluc3AgfiAuICwgc2FsZXMpLAogICAgV29ya2Zsb3coIkxPRi53ZiIsIGs9NyksCiAgICBFc3RpbWF0aW9uVGFzayhtZXRyaWNzPWMoIlByZWNpc2lvbiIsIlJlY2FsbCIsImF2Z05EVFAiKSwKICAgICAgICAgICAgICAgICAgIG1ldGhvZD1Ib2xkb3V0KG5SZXBzPTMsIGhsZFN6PTAuMywgc3RyYXQ9VFJVRSksCiAgICAgICAgICAgICAgICAgICBldmFsdWF0b3I9ImV2YWxPdXRsaWVyUmFua2luZyIsCiAgICAgICAgICAgICAgICAgICBldmFsdWF0b3IucGFycz1saXN0KFRocmVzaG9sZD0wLjEsIHN0YXRzUHJvZHM9Z2xvYmFsU3RhdHMpKQogICAgKQoKYGBgCgoKYGBge3J9CnN1bW1hcnkobG9mLnJlcykKYGBgCgpgYGB7cn0KcHMubG9mIDwtIHNhcHBseShnZXRJdGVyYXRpb25zSW5mbyhsb2YucmVzKSwgZnVuY3Rpb24oaSkgaSRwcm9ic1ssMV0pCnRzLmxvZiA8LSBzYXBwbHkoZ2V0SXRlcmF0aW9uc0luZm8obG9mLnJlcyksIGZ1bmN0aW9uKGkpIGkkcHJvYnNbLDJdKQpQUmN1cnZlKHBzLmJwLHRzLmJwLG1haW49IlBSIGN1cnZlIixsdHk9MSwKICAgICAgICB4bGltPWMoMCwxKSx5bGltPWMoMCwxKSxhdmc9InZlcnRpY2FsIikKUFJjdXJ2ZShwcy5sb2YsdHMubG9mLGFkZD1UUlVFLGx0eT0yLGF2Zz0ndmVydGljYWwnKQpsZWdlbmQoJ3RvcHJpZ2h0JyxjKCdCUHJ1bGUnLCdMT0YnKSxsdHk9YygxLDIpKQoKQ1JjaGFydChwcy5icCx0cy5icCxtYWluPSdDdW11bGF0aXZlIFJlY2FsbCBjdXJ2ZScsCiAgICAgICAgbHR5PTEseGxpbT1jKDAsMSkseWxpbT1jKDAsMSksYXZnPSd2ZXJ0aWNhbCcpCkNSY2hhcnQocHMubG9mLHRzLmxvZixhZGQ9VFJVRSxsdHk9Mixhdmc9J3ZlcnRpY2FsJykKbGVnZW5kKCdib3R0b21yaWdodCcsYygnQlBydWxlJywnTE9GJyksbHR5PWMoMSwyKSkKYGBgCgpgYGB7cn0KcGFyKG1mcm93PWMoMSwyKSkgCnBzLmxvZiA8LSBzYXBwbHkoZ2V0SXRlcmF0aW9uc0luZm8obG9mLnJlcyksIGZ1bmN0aW9uKGkpIGkkcHJvYnNbLDFdKQp0cy5sb2YgPC0gc2FwcGx5KGdldEl0ZXJhdGlvbnNJbmZvKGxvZi5yZXMpLCBmdW5jdGlvbihpKSBpJHByb2JzWywyXSkKUFJjdXJ2ZShwcy5icCwgdHMuYnAsbWFpbj0iUFIgY3VydmUiLGx0eT0xLAogICAgICAgIHhsaW09YygwLDEpLHlsaW09YygwLDEpLGF2Zz0idmVydGljYWwiKQpQUmN1cnZlKHBzLmxvZiwgdHMubG9mLGFkZD1ULGx0eT0yLGF2Zz0ndmVydGljYWwnKQpsZWdlbmQoJ3RvcHJpZ2h0JyxjKCdCUHJ1bGUnLCdMT0YnKSxsdHk9YygxLDIpKQpDUmNoYXJ0KHBzLmJwLHRzLmJwLG1haW49J0N1bXVsYXRpdmUgUmVjYWxsIGN1cnZlJywKICAgICAgICBsdHk9MSx4bGltPWMoMCwxKSx5bGltPWMoMCwxKSxhdmc9J3ZlcnRpY2FsJykKQ1JjaGFydChwcy5sb2YsdHMubG9mLGFkZD1ULGx0eT0yLGF2Zz0ndmVydGljYWwnKQpsZWdlbmQoJ2JvdHRvbXJpZ2h0JyxjKCdCUHJ1bGUnLCdMT0YnKSxsdHk9YygxLDIpKQpgYGAKCmBgYHtyfQpPUmgud2YgPC0gZnVuY3Rpb24oZm9ybSwgdHJhaW4sIHRlc3QsIC4uLikgewogICAgcmVxdWlyZShETXdSMiwgcXVpZXRseT1UUlVFKQogICAgbnRyIDwtIG5yb3codHJhaW4pCiAgICBhbGwgPC0gYXMuZGF0YS5mcmFtZShyYmluZCh0cmFpbix0ZXN0KSkKICAgIE4gPC0gbnJvdyhhbGwpCiAgICB1cHMgPC0gc3BsaXQoYWxsJFVwcmljZSxhbGwkUHJvZCkKICAgIHIgPC0gbGlzdChsZW5ndGg9dXBzKQogICAgZm9yKHUgaW4gc2VxKGFsb25nPXVwcykpIAogICAgICAgIHJbW3VdXSA8LSBpZiAoTlJPVyh1cHNbW3VdXSkgPiAzKSAKICAgICAgICAgICAgICAgICAgICAgIG91dGxpZXJzLnJhbmtpbmcodXBzW1t1XV0pJHByb2Iub3V0bGllcnMKICAgICAgICAgICAgICAgICAgZWxzZSBpZiAoTlJPVyh1cHNbW3VdXSkpIHJlcCgwLE5ST1codXBzW1t1XV0pKSAKICAgICAgICAgICAgICAgICAgZWxzZSBOVUxMCiAgICBhbGwkb3JoIDwtIHZlY3RvcihsZW5ndGg9TikKICAgIHNwbGl0KGFsbCRvcmgsYWxsJFByb2QpIDwtIHIKICAgIGFsbCRvcmhbd2hpY2goIShpcy5pbmZpbml0ZShhbGwkb3JoKSB8IGlzLm5hbihhbGwkb3JoKSkpXSA8LSAKICAgICAgICBTb2Z0TWF4KGFsbCRvcmhbd2hpY2goIShpcy5pbmZpbml0ZShhbGwkb3JoKSB8IGlzLm5hbihhbGwkb3JoKSkpXSkKICAgIHJlcyA8LSBsaXN0KHRlc3RTZXQ9dGVzdCwKICAgICAgICAgICAgICAgIHJhbmtPcmRlcj1vcmRlcihhbGxbKG50cisxKTpOLCdvcmgnXSxkZWNyZWFzaW5nPVRSVUUpLAogICAgICAgICAgICAgICAgcHJvYnM9YXMubWF0cml4KGNiaW5kKGFsbFsobnRyKzEpOk4sJ29yaCddLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGlmZWxzZSh0ZXN0JEluc3A9PSdmcmF1ZCcsMSwwKSkpKQogICAgcmVzCiAgICAKfQpgYGAKCmBgYHtyfQpvcmgucmVzIDwtIHBlcmZvcm1hbmNlRXN0aW1hdGlvbigKICAgIFByZWRUYXNrKEluc3AgfiAuICwgc2FsZXMpLAogICAgV29ya2Zsb3coIk9SaC53ZiIpLAogICAgRXN0aW1hdGlvblRhc2sobWV0cmljcz1jKCJQcmVjaXNpb24iLCJSZWNhbGwiLCJhdmdORFRQIiksCiAgICAgICAgICAgICAgICAgICBtZXRob2Q9SG9sZG91dChuUmVwcz0zLCBobGRTej0wLjMsIHN0cmF0PVRSVUUpLAogICAgICAgICAgICAgICAgICAgZXZhbHVhdG9yPSJldmFsT3V0bGllclJhbmtpbmciLAogICAgICAgICAgICAgICAgICAgZXZhbHVhdG9yLnBhcnM9bGlzdChUaHJlc2hvbGQ9MC4xLCBzdGF0c1Byb2RzPWdsb2JhbFN0YXRzKSkKICAgICkKYGBgCgpgYGB7cn0Kc3VtbWFyeShvcmgucmVzKQoKYGBgCgpgYGB7cn0KcHMub3JoIDwtIHNhcHBseShnZXRJdGVyYXRpb25zSW5mbyhvcmgucmVzKSwgZnVuY3Rpb24oaSkgaSRwcm9ic1ssMV0pCnRzLm9yaCA8LSBzYXBwbHkoZ2V0SXRlcmF0aW9uc0luZm8ob3JoLnJlcyksIGZ1bmN0aW9uKGkpIGkkcHJvYnNbLDJdKQpQUmN1cnZlKHBzLmJwLHRzLmJwLG1haW49IlBSIGN1cnZlIixsdHk9MSwKICAgICAgICB4bGltPWMoMCwxKSx5bGltPWMoMCwxKSxhdmc9InZlcnRpY2FsIikKUFJjdXJ2ZShwcy5sb2YsdHMubG9mLGFkZD1UUlVFLGx0eT0yLGF2Zz0ndmVydGljYWwnKQpQUmN1cnZlKHBzLm9yaCx0cy5vcmgsYWRkPVRSVUUsbHR5PTEsY29sPSdncmV5JywgYXZnPSd2ZXJ0aWNhbCcpCmxlZ2VuZCgndG9wcmlnaHQnLGMoJ0JQcnVsZScsJ0xPRicsJ09SaCcpLGx0eT1jKDEsMiwxKSwKICAgICAgIGNvbD1jKCdibGFjaycsJ2JsYWNrJywnZ3JleScpKQoKQ1JjaGFydChwcy5icCx0cy5icCxtYWluPSdDdW11bGF0aXZlIFJlY2FsbCBjdXJ2ZScsCiAgICAgICAgbHR5PTEseGxpbT1jKDAsMSkseWxpbT1jKDAsMSksYXZnPSd2ZXJ0aWNhbCcpCkNSY2hhcnQocHMubG9mLHRzLmxvZixhZGQ9VFJVRSxsdHk9Mixhdmc9J3ZlcnRpY2FsJykKQ1JjaGFydChwcy5vcmgsdHMub3JoLGFkZD1UUlVFLGx0eT0xLGNvbD0nZ3JleScsYXZnPSd2ZXJ0aWNhbCcpCmxlZ2VuZCgnYm90dG9tcmlnaHQnLGMoJ0JQcnVsZScsJ0xPRicsJ09SaCcpLGx0eT1jKDEsMiwxKSwKICAgICAgIGNvbD1jKCdibGFjaycsJ2JsYWNrJywnZ3JleScpKQpgYGAKCmBgYHtyfQpwYXIobWZyb3c9YygxLDIpKSAKcHMub3JoIDwtIHNhcHBseShnZXRJdGVyYXRpb25zSW5mbyhvcmgucmVzKSwgZnVuY3Rpb24oaSkgaSRwcm9ic1ssMV0pCnRzLm9yaCA8LSBzYXBwbHkoZ2V0SXRlcmF0aW9uc0luZm8ob3JoLnJlcyksIGZ1bmN0aW9uKGkpIGkkcHJvYnNbLDJdKQpQUmN1cnZlKHBzLmJwLHRzLmJwLG1haW49IlBSIGN1cnZlIixsdHk9MSwKICAgICAgICB4bGltPWMoMCwxKSx5bGltPWMoMCwxKSxhdmc9InZlcnRpY2FsIikKUFJjdXJ2ZShwcy5sb2YsdHMubG9mLGFkZD1ULGx0eT0yLGF2Zz0ndmVydGljYWwnKQpQUmN1cnZlKHBzLm9yaCx0cy5vcmgsYWRkPVQsbHR5PTEsY29sPSdncmV5JywgYXZnPSd2ZXJ0aWNhbCcpCmxlZ2VuZCgndG9wcmlnaHQnLGMoJ0JQcnVsZScsJ0xPRicsJ09SaCcpLGx0eT1jKDEsMiwxKSwKICAgICAgIGNvbD1jKCdibGFjaycsJ2JsYWNrJywnZ3JleScpKQoKQ1JjaGFydChwcy5icCx0cy5icCxtYWluPSdDdW11bGF0aXZlIFJlY2FsbCBjdXJ2ZScsCiAgICAgICAgbHR5PTEseGxpbT1jKDAsMSkseWxpbT1jKDAsMSksYXZnPSd2ZXJ0aWNhbCcpCkNSY2hhcnQocHMubG9mLHRzLmxvZixhZGQ9VCxsdHk9Mixhdmc9J3ZlcnRpY2FsJykKQ1JjaGFydChwcy5vcmgsdHMub3JoLGFkZD1ULGx0eT0xLGNvbD0nZ3JleScsYXZnPSd2ZXJ0aWNhbCcpCmxlZ2VuZCgnYm90dG9tcmlnaHQnLGMoJ0JQcnVsZScsJ0xPRicsJ09SaCcpLGx0eT1jKDEsMiwxKSwKICAgICAgIGNvbD1jKCdibGFjaycsJ2JsYWNrJywnZ3JleScpKQpgYGAKCgojIyMjIFN1cGVydmlzZWQgQXBwcm9hY2hlcwoKYGBge3J9CmxpYnJhcnkoVUJMKQpkYXRhKGlyaXMpCmRhdGEgPC0gaXJpc1ssIGMoMSwgMiwgNSldCmRhdGEkU3BlY2llcyA8LSBmYWN0b3IoaWZlbHNlKGRhdGEkU3BlY2llcyA9PSAic2V0b3NhIiwgInJhcmUiLCJjb21tb24iKSkKdGFibGUoZGF0YSRTcGVjaWVzKQpuZXdEYXRhIDwtIFNtb3RlQ2xhc3NpZihTcGVjaWVzIH4gLiwgZGF0YSwgQy5wZXJjID0gImJhbGFuY2UiKQp0YWJsZShuZXdEYXRhJFNwZWNpZXMpCm5ld0RhdGEyIDwtIFNtb3RlQ2xhc3NpZihTcGVjaWVzIH4gLiwgZGF0YSwgQy5wZXJjID0gbGlzdChjb21tb24gPSAxLHJhcmUgPSA2KSkKdGFibGUobmV3RGF0YTIkU3BlY2llcykKYGBgCgoKYGBge3J9CmxpYnJhcnkoZ2dwbG90MikKZ2dwbG90KGRhdGEsYWVzKHg9U2VwYWwuTGVuZ3RoLHk9U2VwYWwuV2lkdGgsY29sb3I9U3BlY2llcykpICsKICAgIGdlb21fcG9pbnQoKSArIGdndGl0bGUoIk9yaWdpbmFsIERhdGEiKQpnZ3Bsb3QobmV3RGF0YTIsYWVzKHg9U2VwYWwuTGVuZ3RoLHk9U2VwYWwuV2lkdGgsY29sb3I9U3BlY2llcykpICsKICAgIGdlb21fcG9pbnQoKSArIGdndGl0bGUoIlNNT1RFJ2QgRGF0YSIpCmBgYApgYGB7cn0KTkIud2YgPC0gZnVuY3Rpb24oZm9ybSx0cmFpbix0ZXN0LC4uLikgewogICAgcmVxdWlyZShlMTA3MSxxdWlldGx5PVRSVUUpCiAgICBzdXAgPC0gd2hpY2godHJhaW4kSW5zcCAhPSAndW5rbicpCiAgICBkYXRhIDwtIGFzLmRhdGEuZnJhbWUodHJhaW5bc3VwLGMoJ0lEJywnUHJvZCcsJ1VwcmljZScsJ0luc3AnKV0pCiAgICBkYXRhJEluc3AgPC0gZmFjdG9yKGRhdGEkSW5zcCxsZXZlbHM9Yygnb2snLCdmcmF1ZCcpKQogICAgbW9kZWwgPC0gbmFpdmVCYXllcyhJbnNwIH4gLixkYXRhLCAuLi4pCiAgICBwcmVkcyA8LSBwcmVkaWN0KG1vZGVsLHRlc3RbLGMoJ0lEJywnUHJvZCcsJ1VwcmljZScsJ0luc3AnKV0sIHR5cGU9J3JhdycpCiAgICByYW5rT3JkZXIgPC0gb3JkZXIocHJlZHNbLCdmcmF1ZCddLCBkZWNyZWFzaW5nPVRSVUUpCiAgICByYW5rU2NvcmUgPC0gcHJlZHNbLCdmcmF1ZCddCiAgICByZXMgPC0gbGlzdCh0ZXN0U2V0PXRlc3QsCiAgICAgICAgICAgICAgICByYW5rT3JkZXI9cmFua09yZGVyLAogICAgICAgICAgICAgICAgcHJvYnM9YXMubWF0cml4KGNiaW5kKHJhbmtTY29yZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpZmVsc2UodGVzdCRJbnNwPT0nZnJhdWQnLDEsMCkpKSkKICAgIHJlcwp9CmBgYAoKYGBge3J9Cm5iLnJlcyA8LSBwZXJmb3JtYW5jZUVzdGltYXRpb24oCiAgICBQcmVkVGFzayhJbnNwIH4gLiAsIHNhbGVzKSwKICAgIFdvcmtmbG93KCJOQi53ZiIpLAogICAgRXN0aW1hdGlvblRhc2sobWV0cmljcz1jKCJQcmVjaXNpb24iLCJSZWNhbGwiLCJhdmdORFRQIiksCiAgICAgICAgICAgICAgICAgICBtZXRob2Q9SG9sZG91dChuUmVwcz0zLGhsZFN6PTAuMyxzdHJhdD1UUlVFKSwKICAgICAgICAgICAgICAgICAgIGV2YWx1YXRvcj0iZXZhbE91dGxpZXJSYW5raW5nIiwKICAgICAgICAgICAgICAgICAgIGV2YWx1YXRvci5wYXJzPWxpc3QoVGhyZXNob2xkPTAuMSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RhdHNQcm9kcz1nbG9iYWxTdGF0cykpCiAgICApCmBgYAoKYGBge3J9CnN1bW1hcnkobmIucmVzKQpgYGAKCmBgYHtyfQpwcy5uYiA8LSBzYXBwbHkoZ2V0SXRlcmF0aW9uc0luZm8obmIucmVzKSwgZnVuY3Rpb24oaSkgaSRwcm9ic1ssMV0pCnRzLm5iIDwtIHNhcHBseShnZXRJdGVyYXRpb25zSW5mbyhuYi5yZXMpLCBmdW5jdGlvbihpKSBpJHByb2JzWywyXSkKUFJjdXJ2ZShwcy5uYix0cy5uYixtYWluPSJQUiBjdXJ2ZSIsbHR5PTEsCiAgICAgICAgeGxpbT1jKDAsMSkseWxpbT1jKDAsMSksYXZnPSJ2ZXJ0aWNhbCIpClBSY3VydmUocHMub3JoLHRzLm9yaCxhZGQ9VFJVRSxsdHk9Mixhdmc9J3ZlcnRpY2FsJykKbGVnZW5kKCd0b3ByaWdodCcsYygnTmFpdmVCYXllcycsJ09SaCcpLGx0eT0xLGNvbD1jKCdibGFjaycsJ2dyZXknKSkKCkNSY2hhcnQocHMubmIsdHMubmIsbWFpbj0nQ3VtdWxhdGl2ZSBSZWNhbGwgY3VydmUnLAogICAgICAgIGx0eT0xLHhsaW09YygwLDEpLHlsaW09YygwLDEpLGF2Zz0ndmVydGljYWwnKQpDUmNoYXJ0KHBzLm9yaCx0cy5vcmgsYWRkPVRSVUUsbHR5PTIsYXZnPSd2ZXJ0aWNhbCcpCmxlZ2VuZCgnYm90dG9tcmlnaHQnLGMoJ05haXZlQmF5ZXMnLCdPUmgnKSxsdHk9MSxjb2w9YygnYmxhY2snLCdncmV5JykpCmBgYAoKYGBge3J9CnBhcihtZnJvdz1jKDEsMikpIApwcy5uYiA8LSBzYXBwbHkoZ2V0SXRlcmF0aW9uc0luZm8obmIucmVzKSwgZnVuY3Rpb24oaSkgaSRwcm9ic1ssMV0pCnRzLm5iIDwtIHNhcHBseShnZXRJdGVyYXRpb25zSW5mbyhuYi5yZXMpLCBmdW5jdGlvbihpKSBpJHByb2JzWywyXSkKUFJjdXJ2ZShwcy5uYix0cy5uYixtYWluPSJQUiBjdXJ2ZSIsbHR5PTEsCiAgICAgICAgeGxpbT1jKDAsMSkseWxpbT1jKDAsMSksYXZnPSJ2ZXJ0aWNhbCIpClBSY3VydmUocHMub3JoLHRzLm9yaCxhZGQ9VCxsdHk9Mixhdmc9J3ZlcnRpY2FsJykKbGVnZW5kKCd0b3ByaWdodCcsYygnTmFpdmVCYXllcycsJ09SaCcpLGx0eT0xLGNvbD1jKCdibGFjaycsJ2dyZXknKSkKCkNSY2hhcnQocHMubmIsdHMubmIsbWFpbj0nQ3VtdWxhdGl2ZSBSZWNhbGwgY3VydmUnLAogICAgICAgIGx0eT0xLHhsaW09YygwLDEpLHlsaW09YygwLDEpLGF2Zz0ndmVydGljYWwnKQpDUmNoYXJ0KHBzLm9yaCx0cy5vcmgsYWRkPVQsbHR5PTIsYXZnPSd2ZXJ0aWNhbCcpCmxlZ2VuZCgnYm90dG9tcmlnaHQnLGMoJ05haXZlQmF5ZXMnLCdPUmgnKSxsdHk9MSxjb2w9YygnYmxhY2snLCdncmV5JykpCgpgYGAKCmBgYHtyfQpOQnNtLndmIDwtIGZ1bmN0aW9uKGZvcm0sdHJhaW4sdGVzdCxDLnBlcmM9ImJhbGFuY2UiLGRpc3Q9IkhFT00iLC4uLikgewogICAgcmVxdWlyZShlMTA3MSxxdWlldGx5PVRSVUUpCiAgICByZXF1aXJlKFVCTCxxdWlldGx5PVRSVUUpCgogICAgc3VwIDwtIHdoaWNoKHRyYWluJEluc3AgIT0gJ3Vua24nKQogICAgZGF0YSA8LSBhcy5kYXRhLmZyYW1lKHRyYWluW3N1cCxjKCdJRCcsJ1Byb2QnLCdVcHJpY2UnLCdJbnNwJyldKQogICAgZGF0YSRJbnNwIDwtIGZhY3RvcihkYXRhJEluc3AsbGV2ZWxzPWMoJ29rJywnZnJhdWQnKSkKICAgIG5ld0RhdGEgPC0gU21vdGVDbGFzc2lmKEluc3AgfiAuLGRhdGEsQy5wZXJjPUMucGVyYyxkaXN0PWRpc3QsLi4uKQogICAgbW9kZWwgPC0gbmFpdmVCYXllcyhJbnNwIH4gLixuZXdEYXRhKQogICAgcHJlZHMgPC0gcHJlZGljdChtb2RlbCx0ZXN0WyxjKCdJRCcsJ1Byb2QnLCdVcHJpY2UnLCdJbnNwJyldLHR5cGU9J3JhdycpCiAgICByYW5rT3JkZXIgPC0gb3JkZXIocHJlZHNbLCdmcmF1ZCddLGRlY3JlYXNpbmc9VCkKICAgIHJhbmtTY29yZSA8LSBwcmVkc1ssJ2ZyYXVkJ10KICAgIAogICAgcmVzIDwtIGxpc3QodGVzdFNldD10ZXN0LAogICAgICAgICAgICAgIHJhbmtPcmRlcj1yYW5rT3JkZXIsCiAgICAgICAgICAgICAgcHJvYnM9YXMubWF0cml4KGNiaW5kKHJhbmtTY29yZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWZlbHNlKHRlc3QkSW5zcD09J2ZyYXVkJywxLDApKSkpCiAgICByZXMKfQpgYGAKCmBgYHtyfQpuYnMucmVzIDwtIHBlcmZvcm1hbmNlRXN0aW1hdGlvbigKICAgIFByZWRUYXNrKEluc3AgfiAuLCBzYWxlcyksCiAgICBXb3JrZmxvdygiTkJzbS53ZiIpLAogICAgRXN0aW1hdGlvblRhc2sobWV0cmljcz1jKCJQcmVjaXNpb24iLCJSZWNhbGwiLCJhdmdORFRQIiksCiAgICAgICAgICAgICAgICAgICBtZXRob2Q9SG9sZG91dChuUmVwcz0zLGhsZFN6PTAuMyxzdHJhdD1UUlVFKSwKICAgICAgICAgICAgICAgICAgIGV2YWx1YXRvcj0iZXZhbE91dGxpZXJSYW5raW5nIiwKICAgICAgICAgICAgICAgICAgIGV2YWx1YXRvci5wYXJzPWxpc3QoVGhyZXNob2xkPTAuMSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RhdHNQcm9kcz1nbG9iYWxTdGF0cykpCiAgICApCmBgYAoKYGBge3J9CnN1bW1hcnkobmJzLnJlcykgCgpgYGAKCmBgYHtyfQpwcy5uYnMgPC0gc2FwcGx5KGdldEl0ZXJhdGlvbnNJbmZvKG5icy5yZXMpLCBmdW5jdGlvbihpKSBpJHByb2JzWywxXSkKdHMubmJzIDwtIHNhcHBseShnZXRJdGVyYXRpb25zSW5mbyhuYnMucmVzKSwgZnVuY3Rpb24oaSkgaSRwcm9ic1ssMl0pClBSY3VydmUocHMubmIsdHMubmIsbWFpbj0iUFIgY3VydmUiLGx0eT0xLAogICAgICAgIHhsaW09YygwLDEpLHlsaW09YygwLDEpLCBhdmc9InZlcnRpY2FsIikKUFJjdXJ2ZShwcy5vcmgsdHMub3JoLGFkZD1UUlVFLGx0eT0yLCBhdmc9J3ZlcnRpY2FsJykKUFJjdXJ2ZShwcy5uYnMsdHMubmJzLGFkZD1UUlVFLGx0eT0xLCBjb2w9J2dyZXknLGF2Zz0ndmVydGljYWwnKQpsZWdlbmQoJ3RvcHJpZ2h0JyxjKCdOYWl2ZUJheWVzJywnT1JoJywnc21vdGVOYWl2ZUJheWVzJyksbHR5PWMoMSwyLDEpLAogICAgICAgY29sPWMoJ2JsYWNrJywnYmxhY2snLCdncmV5JykpCgpDUmNoYXJ0KHBzLm5iLHRzLm5iLG1haW49J0N1bXVsYXRpdmUgUmVjYWxsIGN1cnZlJywKICAgICAgICBsdHk9MSx4bGltPWMoMCwxKSx5bGltPWMoMCwxKSxhdmc9J3ZlcnRpY2FsJykKQ1JjaGFydChwcy5vcmgsdHMub3JoLGFkZD1UUlVFLGx0eT0yLGF2Zz0ndmVydGljYWwnKQpDUmNoYXJ0KHBzLm5icyx0cy5uYnMsYWRkPVRSVUUsbHR5PTEsY29sPSdncmV5Jyxhdmc9J3ZlcnRpY2FsJykKbGVnZW5kKCdib3R0b21yaWdodCcsYygnTmFpdmVCYXllcycsJ09SaCcsJ3Ntb3RlTmFpdmVCYXllcycpLGx0eT1jKDEsMiwxKSwKICAgICAgIGNvbD1jKCdibGFjaycsJ2JsYWNrJywnZ3JleScpKQpgYGAKCgpgYGB7cn0KcGFyKG1mcm93PWMoMSwyKSkgCnBzLm5icyA8LSBzYXBwbHkoZ2V0SXRlcmF0aW9uc0luZm8obmJzLnJlcyksICBmdW5jdGlvbihpKSBpJHByb2JzWywxXSkKdHMubmJzIDwtIHNhcHBseShnZXRJdGVyYXRpb25zSW5mbyhuYnMucmVzKSwgIGZ1bmN0aW9uKGkpIGkkcHJvYnNbLDJdKQpQUmN1cnZlKHBzLm5iLHRzLm5iLG1haW49IlBSIGN1cnZlIixsdHk9MSwKICAgICAgICB4bGltPWMoMCwxKSx5bGltPWMoMCwxKSwgYXZnPSJ2ZXJ0aWNhbCIpClBSY3VydmUocHMub3JoLHRzLm9yaCxhZGQ9VCxsdHk9Mixhdmc9J3ZlcnRpY2FsJykKUFJjdXJ2ZShwcy5uYnMsdHMubmJzLGFkZD1ULGx0eT0xLGNvbD0nZ3JleScsYXZnPSd2ZXJ0aWNhbCcpCmxlZ2VuZCgndG9wcmlnaHQnLGMoJ05haXZlQmF5ZXMnLCdPUmgnLCdzbW90ZU5haXZlQmF5ZXMnKSwKICAgICAgIGx0eT1jKDEsMiwxKSxjb2w9YygnYmxhY2snLCdibGFjaycsJ2dyZXknKSkKCkNSY2hhcnQocHMubmIsdHMubmIsbWFpbj0nQ3VtdWxhdGl2ZSBSZWNhbGwgY3VydmUnLAogICAgICAgIGx0eT0xLHhsaW09YygwLDEpLHlsaW09YygwLDEpLGF2Zz0ndmVydGljYWwnKQpDUmNoYXJ0KHBzLm9yaCx0cy5vcmgsYWRkPVQsbHR5PTIsYXZnPSd2ZXJ0aWNhbCcpCkNSY2hhcnQocHMubmJzLHRzLm5icyxhZGQ9VCxsdHk9MSxjb2w9J2dyZXknLGF2Zz0ndmVydGljYWwnKQpsZWdlbmQoJ2JvdHRvbXJpZ2h0JyxjKCdOYWl2ZUJheWVzJywnT1JoJywnc21vdGVOYWl2ZUJheWVzJyksCiAgICAgICBsdHk9YygxLDIsMSksY29sPWMoJ2JsYWNrJywnYmxhY2snLCdncmV5JykpCmBgYAoKIyMjIyBTZW1pLVN1cGVydmlzZWQgQXBwcm9hY2hlcwoKCmBgYHtyfQpsaWJyYXJ5KERNd1IyKQpsaWJyYXJ5KGUxMDcxKQpkYXRhKGlyaXMpIApzZXQuc2VlZCgxMjM0KQppZHggPC0gc2FtcGxlKDE1MCwgMTAwKQp0ciA8LSBpcmlzW2lkeCwgXQp0cyA8LSBpcmlzWy1pZHgsIF0KbmIgPC0gbmFpdmVCYXllcyhTcGVjaWVzIH4gLiwgdHIpCnRhYmxlKHByZWRpY3QobmIsIHRzKSwgdHMkU3BlY2llcykKdHJTVCA8LSB0cgpuYXMgPC0gc2FtcGxlKDEwMCwgOTApCnRyU1RbbmFzLCAiU3BlY2llcyJdIDwtIE5BCmZ1bmMgPC0gZnVuY3Rpb24obSwgZCkgewogICAgcCA8LSBwcmVkaWN0KG0sIGQsIHR5cGUgPSAicmF3IikKICAgIGRhdGEuZnJhbWUoY2wgPSBjb2xuYW1lcyhwKVsgYXBwbHkocCwgMSwgd2hpY2gubWF4KSBdLCAKICAgICAgICAgICAgICAgcCA9IGFwcGx5KHAsIDEsIG1heCkpCn0KbmJTVGJhc2UgPC0gbmFpdmVCYXllcyhTcGVjaWVzIH4gLiwgdHJTVFstbmFzLCBdKQp0YWJsZShwcmVkaWN0KG5iU1RiYXNlLCB0cyksIHRzJFNwZWNpZXMpCm5iU1QgPC0gU2VsZlRyYWluKFNwZWNpZXMgfiAuLCB0clNULCAKICAgICAgICAgICAgICAgICAgbGVhcm5lcj0ibmFpdmVCYXllcyIsIGxlYXJuZXIucGFycz1saXN0KCksCiAgICAgICAgICAgICAgICAgIHByZWQ9ImZ1bmMiKQp0YWJsZShwcmVkaWN0KG5iU1QsIHRzKSwgdHMkU3BlY2llcykKYGBgCgpgYGB7cn0KcHJlZC5uYiA8LSBmdW5jdGlvbihtLGQpIHsKICAgIHAgPC0gcHJlZGljdChtLGQsdHlwZT0ncmF3JykKICAgIGRhdGEuZnJhbWUoY2w9Y29sbmFtZXMocClbYXBwbHkocCwxLHdoaWNoLm1heCldLAogICAgICAgICAgICAgICBwPWFwcGx5KHAsMSxtYXgpCiAgICAgICAgICAgICAgICkKfQogCm5iLnN0LndmIDwtIGZ1bmN0aW9uKGZvcm0sdHJhaW4sdGVzdCwuLi4pIHsKICAgIHJlcXVpcmUoZTEwNzEscXVpZXRseT1UUlVFKQogICAgcmVxdWlyZShETXdSMiwgcXVpZXRseT1UUlVFKQogICAgdHJhaW4gPC0gYXMuZGF0YS5mcmFtZSh0cmFpblssYygnSUQnLCdQcm9kJywnVXByaWNlJywnSW5zcCcpXSkKICAgIHRyYWluW3doaWNoKHRyYWluJEluc3AgPT0gJ3Vua24nKSwnSW5zcCddIDwtIE5BCiAgICB0cmFpbiRJbnNwIDwtIGZhY3Rvcih0cmFpbiRJbnNwLGxldmVscz1jKCdvaycsJ2ZyYXVkJykpCiAgICBtb2RlbCA8LSBTZWxmVHJhaW4oZm9ybSx0cmFpbiwKICAgICAgICAgICAgICAgICAgICAgICBsZWFybmVyPSduYWl2ZUJheWVzJywgbGVhcm5lci5wYXJzPWxpc3QoKSwKICAgICAgICAgICAgICAgICAgICAgICBwcmVkPSdwcmVkLm5iJykKICAgIHByZWRzIDwtIHByZWRpY3QobW9kZWwsdGVzdFssYygnSUQnLCdQcm9kJywnVXByaWNlJywnSW5zcCcpXSwKICAgICAgICAgICAgICAgICAgICAgdHlwZT0ncmF3JykKCiAgICByYW5rT3JkZXIgPC0gb3JkZXIocHJlZHNbLCdmcmF1ZCddLGRlY3JlYXNpbmc9VFJVRSkKICAgIHJhbmtTY29yZSA8LSBwcmVkc1ssImZyYXVkIl0KICAgIAogICAgcmVzIDwtIGxpc3QodGVzdFNldD10ZXN0LAogICAgICAgICAgICAgIHJhbmtPcmRlcj1yYW5rT3JkZXIsCiAgICAgICAgICAgICAgcHJvYnM9YXMubWF0cml4KGNiaW5kKHJhbmtTY29yZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWZlbHNlKHRlc3QkSW5zcD09J2ZyYXVkJywxLDApKSkpCiAgICByZXMKfQpgYGAKCmBgYHtyfQpuYi5zdC5yZXMgPC0gcGVyZm9ybWFuY2VFc3RpbWF0aW9uKAogICAgUHJlZFRhc2soSW5zcCB+IC4sc2FsZXMpLAogICAgV29ya2Zsb3coIm5iLnN0LndmIiksCiAgICBFc3RpbWF0aW9uVGFzayhtZXRyaWNzPWMoIlByZWNpc2lvbiIsIlJlY2FsbCIsImF2Z05EVFAiKSwKICAgICAgICAgICAgICAgICAgIG1ldGhvZD1Ib2xkb3V0KG5SZXBzPTMsaGxkU3o9MC4zLHN0cmF0PVRSVUUpLAogICAgICAgICAgICAgICAgICAgZXZhbHVhdG9yPSJldmFsT3V0bGllclJhbmtpbmciLAogICAgICAgICAgICAgICAgICAgZXZhbHVhdG9yLnBhcnM9bGlzdChUaHJlc2hvbGQ9MC4xLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGF0c1Byb2RzPWdsb2JhbFN0YXRzKSkKICAgICkKCmBgYAoKYGBge3J9CnN1bW1hcnkobmIuc3QucmVzKQoKYGBgCgoKYGBge3J9CnBzLm5iLnN0IDwtIHNhcHBseShnZXRJdGVyYXRpb25zSW5mbyhuYi5zdC5yZXMpLCBmdW5jdGlvbihpKSBpJHByb2JzWywxXSkKdHMubmIuc3QgPC0gc2FwcGx5KGdldEl0ZXJhdGlvbnNJbmZvKG5iLnN0LnJlcyksIGZ1bmN0aW9uKGkpIGkkcHJvYnNbLDJdKQpQUmN1cnZlKHBzLm5iLHRzLm5iLG1haW49IlBSIGN1cnZlIixsdHk9MSwKICAgICAgICB4bGltPWMoMCwxKSx5bGltPWMoMCwxKSwgYXZnPSJ2ZXJ0aWNhbCIpClBSY3VydmUocHMub3JoLHRzLm9yaCxhZGQ9VFJVRSxsdHk9MSwgY29sb3I9J2dyZXknLCBhdmc9J3ZlcnRpY2FsJykKUFJjdXJ2ZShwcy5uYi5zdCx0cy5uYi5zdCxhZGQ9VFJVRSxsdHk9Mixhdmc9J3ZlcnRpY2FsJykKbGVnZW5kKCd0b3ByaWdodCcsYygnTmFpdmVCYXllcycsJ09SaCcsJ05haXZlQmF5ZXMtU1QnKSwKICAgICAgIGx0eT1jKDEsMSwyKSxjb2w9YygnYmxhY2snLCdncmV5JywnYmxhY2snKSkKCkNSY2hhcnQocHMubmIsdHMubmIsbWFpbj0nQ3VtdWxhdGl2ZSBSZWNhbGwgY3VydmUnLAogICAgICAgIGx0eT0xLHhsaW09YygwLDEpLHlsaW09YygwLDEpLGF2Zz0ndmVydGljYWwnKQpDUmNoYXJ0KHBzLm9yaCx0cy5vcmgsYWRkPVRSVUUsbHR5PTEsY29sb3I9J2dyZXknLGF2Zz0ndmVydGljYWwnKQpDUmNoYXJ0KHBzLm5iLnN0LHRzLm5iLnN0LGFkZD1UUlVFLGx0eT0yLGF2Zz0ndmVydGljYWwnKQpsZWdlbmQoJ2JvdHRvbXJpZ2h0JyxjKCdOYWl2ZUJheWVzJywnT1JoJywnTmFpdmVCYXllcy1TVCcpLAogICAgICAgbHR5PWMoMSwxLDIpLGNvbD1jKCdibGFjaycsJ2dyZXknLCdncmV5JykpCgpgYGAKCmBgYHtyfQpwYXIobWZyb3c9YygxLDIpKSAKcHMubmIuc3QgPC0gc2FwcGx5KGdldEl0ZXJhdGlvbnNJbmZvKG5iLnN0LnJlcyksIGZ1bmN0aW9uKGkpIGkkcHJvYnNbLDFdKQp0cy5uYi5zdCA8LSBzYXBwbHkoZ2V0SXRlcmF0aW9uc0luZm8obmIuc3QucmVzKSwgZnVuY3Rpb24oaSkgaSRwcm9ic1ssMl0pClBSY3VydmUocHMubmIsdHMubmIsbWFpbj0iUFIgY3VydmUiLGx0eT0xLAogICAgICAgIHhsaW09YygwLDEpLHlsaW09YygwLDEpLCBhdmc9InZlcnRpY2FsIikKUFJjdXJ2ZShwcy5vcmgsdHMub3JoLGFkZD1ULGx0eT0xLCBjb2xvcj0nZ3JleScsIGF2Zz0ndmVydGljYWwnKQpQUmN1cnZlKHBzLm5iLnN0LHRzLm5iLnN0LGFkZD1ULGx0eT0yLGF2Zz0ndmVydGljYWwnKQpsZWdlbmQoJ3RvcHJpZ2h0JyxjKCdOYWl2ZUJheWVzJywnT1JoJywnTmFpdmVCYXllcy1TVCcpLAogICAgICAgbHR5PWMoMSwxLDIpLGNvbD1jKCdibGFjaycsJ2dyZXknLCdibGFjaycpKQoKQ1JjaGFydChwcy5uYix0cy5uYixtYWluPSdDdW11bGF0aXZlIFJlY2FsbCBjdXJ2ZScsCiAgICAgICAgbHR5PTEseGxpbT1jKDAsMSkseWxpbT1jKDAsMSksYXZnPSd2ZXJ0aWNhbCcpCkNSY2hhcnQocHMub3JoLHRzLm9yaCxhZGQ9VCxsdHk9MSxjb2xvcj0nZ3JleScsYXZnPSd2ZXJ0aWNhbCcpCkNSY2hhcnQocHMubmIuc3QsdHMubmIuc3QsYWRkPVQsbHR5PTIsYXZnPSd2ZXJ0aWNhbCcpCmxlZ2VuZCgnYm90dG9tcmlnaHQnLGMoJ05haXZlQmF5ZXMnLCdPUmgnLCdOYWl2ZUJheWVzLVNUJyksCiAgICAgICBsdHk9YygxLDEsMiksY29sPWMoJ2JsYWNrJywnZ3JleScsJ2dyZXknKSkKCmBgYAoK