1.0 Introduction

I want to explore the use of decision trees to predict the severity (malignant or benign) of a mass detected during a mammogram. I think it’s a really beneficial application of machine learning, as lower False Positive Rates (FPR) can not only potentially save a lot of time, effort, and resources to the healthcare system, but they could also spare patients from what must be an almost unbearable amount of grief and stress.

2.0 Getting ahold of the data

2.1 Downloading the data

The data was downloaded from UC-Irvine’s Machine Learning Repository.

2.2 Data glossary

Information about the various attributes can be found here.

2.3 Reading the data

Now let’s read the data. Since the data does not have column names, we will add those using the data layout file mentioned earlier.

mammo_data <- read.csv("mammographic_masses.data", header = FALSE)
colnames(mammo_data) <- c("Assessment", "Age", "Shape", "Margin", "Density", "Severity")


Take a look at the data summary

str(mammo_data)
'data.frame':   961 obs. of  6 variables:
 $ Assessment: Factor w/ 8 levels "?","0","2","3",..: 6 5 6 5 6 5 5 6 6 6 ...
 $ Age       : Factor w/ 74 levels "?","18","19",..: 51 27 42 12 58 49 54 26 41 44 ...
 $ Shape     : Factor w/ 5 levels "?","1","2","3",..: 4 2 5 2 2 2 1 2 2 1 ...
 $ Margin    : Factor w/ 6 levels "?","1","2","3",..: 6 2 6 2 6 1 1 1 6 6 ...
 $ Density   : Factor w/ 5 levels "?","1","2","3",..: 4 1 4 4 1 4 4 4 4 2 ...
 $ Severity  : int  1 1 1 0 1 0 0 0 1 1 ...


\(Assessment\) is the BI-RADS classification scheme to summarize the mammography’s results with a single number, ranging from 0 to 6. \(Age\) is the patient’s age, \(Shape\), \(Margin\), and \(Density\) are physical attributes of the mass detected that have been mapped to scales of 1 to 4 or 1 to 5, and \(Severity\) is the Boolean response variable, \(0\) if the mass turned out to be benign, and \(1\) otherwise.

3.0 Data exploration and munging

3.1 Imputing missing data

The \(?\) values of the attributes in the str() printout indicate missing data. We can impute the missing data using the mice package.

First let’s replace the \(?\) with \(NA\).

mammo_data$Assessment[mammo_data$Assessment == "?"] <- NA
mammo_data$Assessment <- factor(mammo_data$Assessment)
mammo_data$Age[mammo_data$Age == "?"] <- NA
mammo_data$Age <- as.integer(mammo_data$Age) # Age is an integer
mammo_data$Shape[mammo_data$Shape == "?"] <- NA
mammo_data$Shape <- factor(mammo_data$Shape)
mammo_data$Margin[mammo_data$Margin == "?"] <- NA
mammo_data$Margin <- factor(mammo_data$Margin)
mammo_data$Density[mammo_data$Density == "?"] <- NA
mammo_data$Density <- factor(mammo_data$Density)
str(mammo_data)
'data.frame':   961 obs. of  6 variables:
 $ Assessment: Factor w/ 7 levels "0","2","3","4",..: 5 4 5 4 5 4 4 5 5 5 ...
 $ Age       : int  51 27 42 12 58 49 54 26 41 44 ...
 $ Shape     : Factor w/ 4 levels "1","2","3","4": 3 1 4 1 1 1 NA 1 1 NA ...
 $ Margin    : Factor w/ 5 levels "1","2","3","4",..: 5 1 5 1 5 NA NA NA 5 5 ...
 $ Density   : Factor w/ 4 levels "1","2","3","4": 3 NA 3 3 NA 3 3 3 3 1 ...
 $ Severity  : int  1 1 1 0 1 0 0 0 1 1 ...


Now we can impute the missing data. This takes a couple of minutes in my computer.

library(mice)
package 㤼㸱mice㤼㸲 was built under R version 3.1.3Loading required package: Rcpp
package 㤼㸱Rcpp㤼㸲 was built under R version 3.1.3mice 2.25 2015-11-09
set.seed(1000)
vars.for.imputation <- c("Assessment", "Age", "Shape", "Margin", "Density")
imputed <- complete(mice(mammo_data[vars.for.imputation]))

 iter imp variable
  1   1  Assessment  Age  Shape  Margin  Density
  1   2  Assessment  Age  Shape  Margin  Density
  1   3  Assessment  Age  Shape  Margin  Density
  1   4  Assessment  Age  Shape  Margin  Density
  1   5  Assessment  Age  Shape  Margin  Density
  2   1  Assessment  Age  Shape  Margin  Density
  2   2  Assessment  Age  Shape  Margin  Density
  2   3  Assessment  Age  Shape  Margin  Density
  2   4  Assessment  Age  Shape  Margin  Density
  2   5  Assessment  Age  Shape  Margin  Density
  3   1  Assessment  Age  Shape  Margin  Density
  3   2  Assessment  Age  Shape  Margin  Density
  3   3  Assessment  Age  Shape  Margin  Density
  3   4  Assessment  Age  Shape  Margin  Density
  3   5  Assessment  Age  Shape  Margin  Density
  4   1  Assessment  Age  Shape  Margin  Density
  4   2  Assessment  Age  Shape  Margin  Density
  4   3  Assessment  Age  Shape  Margin  Density
  4   4  Assessment  Age  Shape  Margin  Density
  4   5  Assessment  Age  Shape  Margin  Density
  5   1  Assessment  Age  Shape  Margin  Density
  5   2  Assessment  Age  Shape  Margin  Density
  5   3  Assessment  Age  Shape  Margin  Density
  5   4  Assessment  Age  Shape  Margin  Density
  5   5  Assessment  Age  Shape  Margin  Density
mammo_data[vars.for.imputation] <- imputed

3.2 Data munging

\(Assessment\) is supposed to be an ordinal variable that can take values from \(0\) to \(6\), but str() shows 7 levels.

table(mammo_data$Assessment)

  0   2   3   4   5  55   6 
  5  15  36 547 346   1  11 


There is a value of \(55\). This could be a case of hitting the ‘5’ key twice by accident.

mammo_data$Severity[mammo_data$Assessment == "55"]
[1] 1


The response for that specific case was \(1\), i.e., the mass turned out to be malignant. We can change the \(55\) to a \(5\).

mammo_data$Assessment[mammo_data$Assessment == "55"] <- "5"
# Reset factor levels
# https://stackoverflow.com/questions/1195826/drop-factor-levels-in-a-subsetted-data-frame
mammo_data$Assessment <- factor(mammo_data$Assessment) 
table(mammo_data$Assessment)

  0   2   3   4   5   6 
  5  15  36 547 347  11 


The cases classified as \(6\) are cases known to be malignant.

We can also convert \(Severity\) to a factor, i.e., an ordinal variable.

mammo_data$Severity <- factor(mammo_data$Severity)
summary(mammo_data)
 Assessment      Age        Shape   Margin  Density Severity
 0:  5      Min.   : 2.00   1:227   1:383   1: 19   0:516   
 2: 15      1st Qu.:29.00   2:217   2: 26   2: 65   1:445   
 3: 36      Median :41.00   3: 97   3:119   3:862           
 4:547      Mean   :39.55   4:420   4:293   4: 15           
 5:347      3rd Qu.:50.00           5:140                   
 6: 11      Max.   :74.00                                   


3.3 Data exploration

A quick look at the summary() printout above shows that the vast majority of \(Assessment\) values are either \(4\) or \(5\). According to the BI-RADS scale, \(4\) is a mass described as a “suspicious abnormality”, whereas a \(5\) is one “highly suspicious of malignancy”. We can split the data into benign and malign and see how the assessments are distributed.

mammo_data_benign <- mammo_data[mammo_data$Severity == "0",]
summary(mammo_data_benign$Assessment)
  0   2   3   4   5   6 
  2  13  30 427  41   3 


\(41\) masses out of \(347\) deemed highly suspicious of being malignant turned out to be benign, and \(427\) out of \(547\) cases characterized as “suspicious” also turned out to be benign. Altogether, \(468\) cases out of \(894\), or \(52.3\%\) of the cases regarded as “suspicious” or “highly suspicious”, were in fact benign. That is a high percentage of people that are potentially under a lot of strain for days or even weeks. There are also 3 cases classified as \(6\) that turned out not to be malignant, after all, which is a bit mystifying, since those are supposed to be cases known to be malignant, according to the BI-RADS scheme.

mammo_data_malign <- mammo_data[mammo_data$Severity == "1",]
summary(mammo_data_malign$Assessment)
  0   2   3   4   5   6 
  3   2   6 120 306   8 


\(8\) cases out of a total of \(51\) classified as “benign” (\(2\)) or “probably benign” (\(3\)) were actually malignant, or about \(15.7\%\).

3.4 Splitting into training and testing datasets

Let’s split the data into training and testing data sets.

library(caTools)
set.seed(1000) # reproducibility
split <- sample.split(mammo_data$Severity, SplitRatio = 0.7)
mammo_data_train <- subset(mammo_data, split==TRUE)
mammo_data_test <- subset(mammo_data, split==FALSE)


4.0 Decision trees

Loading the required libraries

library(rpart)
library(rpart.plot)


Now we build the tree and plot it. The parameter minbucket allows us to select how the minimum number of that are placed in one of the tree’s “leaves” or “buckets”.

# Building a tree with a minimum of 10 observations on each leaf
mammog_tree <- rpart(Severity ~ ., data = mammo_data_train, control=rpart.control(minbucket=10))
prp(mammog_tree)


The tree is not hard to interpret. If \(Assessment\) is \(5\) or \(0\) or \(6\), the tree predicts \(1\), or malignant. Otherwise, the tree looks at \(Shape\) and \(Age\) to predict an outcome. Having built the tree, we can check its accuracy on the training set itself.

# Generate predictions on training set
PredictCART_train = predict(mammog_tree, type = "class")
# Confusion matrix of training set
conf_matrix_train <- table(mammo_data_train$Severity, PredictCART_train)
conf_matrix_train
   PredictCART_train
      0   1
  0 318  43
  1  55 257


The rows are the ground truth whereas the columns are the predictions generated by the tree. For example, out of those training set observations that were benign, the tree correctly labels \(318\) of them as such and \(43\) of them incorrectly as malign. We can compute the accuracy as the sum of the true negatives and true positives and dividing by the total number of observations.

\[ Accuracy = \frac{True\ Positives\ +\ True\ Negatives}{True\ Positives\ +\ True\ Negatives\ +\ False\ Positives\ +\ False\ Negatives} = \frac{Number\ of\ cases\ labelled\ correctly}{Total\ number\ of\ cases} \]

sum(diag(conf_matrix_train)) / sum(conf_matrix_train)
[1] 0.8543834


4.1 Make predictions on the test set using the CART model

We can then make predictions using the tree on the test set.

# Generate predictions on test set
PredictCART_test = predict(mammog_tree, newdata = mammo_data_test, type = "class")
conf_matrix_test <- table(mammo_data_test$Severity, PredictCART_test)
conf_matrix_test
   PredictCART_test
      0   1
  0 133  22
  1  37  96


Accuracy on the test set:

sum(diag(conf_matrix_test)) / sum(conf_matrix_test)
[1] 0.7951389


The accuracy on the test set is a little worse.

4.2 Cross-validation

We can use cross-validation to obtain the tree that maximizes accuracy. There is a parameter called cp, or complexity parameter, that allows us, indirectly, to specify how many observations will be placed on each leaf. We can do 10-fold cross-validation for each cp value, compute the average accuracy of each cp value, and then take the best cp value and build the final tree with it.

Let’s load the libraries we will need for the cross-validation:

library(caret)
library(e1071)


Now define the parameters of the cross-validation. We will do 10-fold CV for each of 50 values of cp, from \(0.01\) to \(0.50\).

# Setting cross-validation to be 10-fold
fitControl = trainControl( method = "cv", number = 10 )
# Setting cp to .01, .02, ..., 0.5
cartGrid = expand.grid( .cp = (1:50)*0.01) 


Perform the cross-validation to determine the best cp parameter. The train() function does 10-fold CV for each cp value, so it builds 10 trees for each cp and computes the average accuracy associated with that cp. Since it does this for each value of cp, train() builds 500 trees. It takes a little while.

set.seed(100)
train(Severity ~ ., data = mammo_data_train, method = "rpart", trControl = fitControl, tuneGrid = cartGrid)
CART 

673 samples
  5 predictor
  2 classes: '0', '1' 

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 605, 606, 606, 605, 606, 606, ... 
Resampling results across tuning parameters:

  cp    Accuracy   Kappa    
  0.01  0.8470588  0.6906251
  0.02  0.8470588  0.6906251
  0.03  0.8322432  0.6596273
  0.04  0.8263169  0.6457520
  0.05  0.8263169  0.6457520
  0.06  0.8263169  0.6457520
  0.07  0.8263169  0.6457520
  0.08  0.8263169  0.6457520
  0.09  0.8263169  0.6457520
  0.10  0.8263169  0.6457520
  0.11  0.8263169  0.6457520
  0.12  0.8263169  0.6457520
  0.13  0.8263169  0.6457520
  0.14  0.8263169  0.6457520
  0.15  0.8263169  0.6457520
  0.16  0.8263169  0.6457520
  0.17  0.8263169  0.6457520
  0.18  0.8263169  0.6457520
  0.19  0.8263169  0.6457520
  0.20  0.8263169  0.6457520
  0.21  0.8263169  0.6457520
  0.22  0.8263169  0.6457520
  0.23  0.8263169  0.6457520
  0.24  0.8263169  0.6457520
  0.25  0.8263169  0.6457520
  0.26  0.8263169  0.6457520
  0.27  0.8263169  0.6457520
  0.28  0.8263169  0.6457520
  0.29  0.8263169  0.6457520
  0.30  0.8263169  0.6457520
  0.31  0.8263169  0.6457520
  0.32  0.8263169  0.6457520
  0.33  0.8263169  0.6457520
  0.34  0.8263169  0.6457520
  0.35  0.8263169  0.6457520
  0.36  0.8263169  0.6457520
  0.37  0.8263169  0.6457520
  0.38  0.8263169  0.6457520
  0.39  0.8263169  0.6457520
  0.40  0.8263169  0.6457520
  0.41  0.8263169  0.6457520
  0.42  0.8263169  0.6457520
  0.43  0.8263169  0.6457520
  0.44  0.8263169  0.6457520
  0.45  0.8263169  0.6457520
  0.46  0.8263169  0.6457520
  0.47  0.8263169  0.6457520
  0.48  0.8263169  0.6457520
  0.49  0.8263169  0.6457520
  0.50  0.8263169  0.6457520

Accuracy was used to select the optimal model using  the largest value.
The final value used for the model was cp = 0.02. 


The train() function from the caret package selects the best value of cp: \(0.02\). We can then use this value to build the final tree:

mammog_tree_cv <- rpart(Severity ~ ., data = mammo_data_train, control=rpart.control(cp=0.02))
prp(mammog_tree_cv)


We get the same tree as before. This tree has about 80% accuracy on the test set. For the cases classified as \(5\), it automatically classifies them as malignant, even though we saw earlier that quite a few of those indeed were not. That’s a bit disappointing.

4.3 Building a tree using only the physical attributes

What if we were to build a tree using just the physical attributes of the mass?

set.seed(100)
train(Severity ~ Shape + Margin + Density, data = mammo_data_train, method = "rpart", trControl = fitControl, tuneGrid = cartGrid)
CART 

673 samples
  5 predictor
  2 classes: '0', '1' 

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 605, 606, 606, 605, 606, 606, ... 
Resampling results across tuning parameters:

  cp    Accuracy   Kappa    
  0.01  0.7801580  0.5606255
  0.02  0.7697542  0.5382077
  0.03  0.7787094  0.5548124
  0.04  0.7757463  0.5484527
  0.05  0.7757463  0.5484527
  0.06  0.7757463  0.5484527
  0.07  0.7757463  0.5484527
  0.08  0.7757463  0.5484527
  0.09  0.7757463  0.5484527
  0.10  0.7757463  0.5484527
  0.11  0.7757463  0.5484527
  0.12  0.7757463  0.5484527
  0.13  0.7757463  0.5484527
  0.14  0.7757463  0.5484527
  0.15  0.7757463  0.5484527
  0.16  0.7757463  0.5484527
  0.17  0.7757463  0.5484527
  0.18  0.7757463  0.5484527
  0.19  0.7757463  0.5484527
  0.20  0.7757463  0.5484527
  0.21  0.7757463  0.5484527
  0.22  0.7757463  0.5484527
  0.23  0.7757463  0.5484527
  0.24  0.7757463  0.5484527
  0.25  0.7757463  0.5484527
  0.26  0.7757463  0.5484527
  0.27  0.7757463  0.5484527
  0.28  0.7757463  0.5484527
  0.29  0.7757463  0.5484527
  0.30  0.7757463  0.5484527
  0.31  0.7757463  0.5484527
  0.32  0.7757463  0.5484527
  0.33  0.7757463  0.5484527
  0.34  0.7757463  0.5484527
  0.35  0.7757463  0.5484527
  0.36  0.7757463  0.5484527
  0.37  0.7757463  0.5484527
  0.38  0.7757463  0.5484527
  0.39  0.7757463  0.5484527
  0.40  0.7757463  0.5484527
  0.41  0.7757463  0.5484527
  0.42  0.7757463  0.5484527
  0.43  0.7757463  0.5484527
  0.44  0.7757463  0.5484527
  0.45  0.7757463  0.5484527
  0.46  0.7757463  0.5484527
  0.47  0.7757463  0.5484527
  0.48  0.7757463  0.5484527
  0.49  0.7757463  0.5484527
  0.50  0.7757463  0.5484527

Accuracy was used to select the optimal model using  the largest value.
The final value used for the model was cp = 0.01. 


Building the tree with the cp value picked by train()

mammog_tree_cv_physical <- rpart(Severity ~ Shape + Margin + Density, data = mammo_data_train, control=rpart.control(cp=0.01))
prp(mammog_tree_cv_physical)


Computing the accuracy of the tree on the test set

PredictCART_test_CV_physical = predict(mammog_tree_cv_physical, newdata = mammo_data_test, type = "class")
conf_matrix_test_CV_physical <- table(mammo_data_test$Severity, PredictCART_test_CV_physical)
conf_matrix_test_CV_physical
   PredictCART_test_CV_physical
      0   1
  0 117  38
  1  25 108


sum(diag(conf_matrix_test_CV_physical)) / sum(conf_matrix_test_CV_physical)
[1] 0.78125


The accuracy of this tree is almost the same as that of the tree that includes the BIRADS assessment.

Overall, I think this dataset would have been more useful if it had more predictors available. More specifically, more physical attributes from the mass could perhaps enable us to see if the BI-RADS assessment could be improved upon. We could also build a tree to try to predict the \(Assessment\) of the radiologist, who uses many more factors than simply shape, margin, and density to determine classification.

References

  1. Bertsimas, D., O’Hair, A. The Analytics Edge. Spring 2014. edX.org.
LS0tDQp0aXRsZTogIlVzaW5nIGRlY2lzaW9uIHRyZWVzIHRvIHByZWRpY3QgdGhlIHNldmVyaXR5IG9mIG1hbW1vZ3JhcGhpYyBtYXNzZXMiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQojIyAxLjAgSW50cm9kdWN0aW9uDQoNCkkgd2FudCB0byBleHBsb3JlIHRoZSB1c2Ugb2YgZGVjaXNpb24gdHJlZXMgdG8gcHJlZGljdCB0aGUgc2V2ZXJpdHkgKG1hbGlnbmFudCBvciBiZW5pZ24pIG9mIGEgbWFzcyBkZXRlY3RlZCBkdXJpbmcgYSBtYW1tb2dyYW0uIEkgdGhpbmsgaXQncyBhIHJlYWxseSBiZW5lZmljaWFsIGFwcGxpY2F0aW9uIG9mIG1hY2hpbmUgbGVhcm5pbmcsIGFzIGxvd2VyIEZhbHNlIFBvc2l0aXZlIFJhdGVzIChGUFIpIGNhbiBub3Qgb25seSBwb3RlbnRpYWxseSBzYXZlIGEgbG90IG9mIHRpbWUsIGVmZm9ydCwgYW5kIHJlc291cmNlcyB0byB0aGUgaGVhbHRoY2FyZSBzeXN0ZW0sIGJ1dCB0aGV5IGNvdWxkIGFsc28gc3BhcmUgcGF0aWVudHMgZnJvbSB3aGF0IG11c3QgYmUgYW4gYWxtb3N0ICoqdW5iZWFyYWJsZSoqIGFtb3VudCBvZiBncmllZiBhbmQgc3RyZXNzLg0KPGJyPg0KDQojIyAyLjAgR2V0dGluZyBhaG9sZCBvZiB0aGUgZGF0YQ0KDQojIyMgMi4xIERvd25sb2FkaW5nIHRoZSBkYXRhDQoNClRoZSBkYXRhIHdhcyBkb3dubG9hZGVkIGZyb20gVUMtSXJ2aW5lJ3MgW01hY2hpbmUgTGVhcm5pbmcgUmVwb3NpdG9yeV0oaHR0cDovL2FyY2hpdmUuaWNzLnVjaS5lZHUvbWwvZGF0YXNldHMvbWFtbW9ncmFwaGljK21hc3MpLiANCjxicj4NCg0KIyMjIDIuMiBEYXRhIGdsb3NzYXJ5DQoNCkluZm9ybWF0aW9uIGFib3V0IHRoZSB2YXJpb3VzIGF0dHJpYnV0ZXMgY2FuIGJlIGZvdW5kIFtoZXJlXShodHRwOi8vYXJjaGl2ZS5pY3MudWNpLmVkdS9tbC9tYWNoaW5lLWxlYXJuaW5nLWRhdGFiYXNlcy9tYW1tb2dyYXBoaWMtbWFzc2VzL21hbW1vZ3JhcGhpY19tYXNzZXMubmFtZXMpLg0KPGJyPg0KDQojIyMgMi4zIFJlYWRpbmcgdGhlIGRhdGENCg0KTm93IGxldCdzIHJlYWQgdGhlIGRhdGEuIFNpbmNlIHRoZSBkYXRhIGRvZXMgbm90IGhhdmUgY29sdW1uIG5hbWVzLCB3ZSB3aWxsIGFkZCB0aG9zZSB1c2luZyB0aGUgZGF0YSBsYXlvdXQgZmlsZSBtZW50aW9uZWQgZWFybGllci4NCmBgYHtyfQ0KbWFtbW9fZGF0YSA8LSByZWFkLmNzdigibWFtbW9ncmFwaGljX21hc3Nlcy5kYXRhIiwgaGVhZGVyID0gRkFMU0UpDQpjb2xuYW1lcyhtYW1tb19kYXRhKSA8LSBjKCJBc3Nlc3NtZW50IiwgIkFnZSIsICJTaGFwZSIsICJNYXJnaW4iLCAiRGVuc2l0eSIsICJTZXZlcml0eSIpDQpgYGANCjxicj4NCg0KVGFrZSBhIGxvb2sgYXQgdGhlIGRhdGEgc3VtbWFyeQ0KYGBge3J9DQpzdHIobWFtbW9fZGF0YSkNCmBgYA0KPGJyPg0KDQokQXNzZXNzbWVudCQgaXMgdGhlIFtCSS1SQURTXShodHRwOi8vYnJlYXN0LWNhbmNlci5jYS9iaS1yYWRzLykgY2xhc3NpZmljYXRpb24gc2NoZW1lIHRvIHN1bW1hcml6ZSB0aGUgbWFtbW9ncmFwaHkncyByZXN1bHRzIHdpdGggYSBzaW5nbGUgbnVtYmVyLCByYW5naW5nIGZyb20gMCB0byA2LiAkQWdlJCBpcyB0aGUgcGF0aWVudCdzIGFnZSwgJFNoYXBlJCwgJE1hcmdpbiQsIGFuZCAkRGVuc2l0eSQgYXJlIHBoeXNpY2FsIGF0dHJpYnV0ZXMgb2YgdGhlIG1hc3MgZGV0ZWN0ZWQgdGhhdCBoYXZlIGJlZW4gbWFwcGVkIHRvIHNjYWxlcyBvZiAxIHRvIDQgb3IgMSB0byA1LCBhbmQgJFNldmVyaXR5JCBpcyB0aGUgQm9vbGVhbiByZXNwb25zZSB2YXJpYWJsZSwgJDAkIGlmIHRoZSBtYXNzIHR1cm5lZCBvdXQgdG8gYmUgYmVuaWduLCBhbmQgJDEkIG90aGVyd2lzZS4NCjxicj4NCg0KIyMgMy4wIERhdGEgZXhwbG9yYXRpb24gYW5kIG11bmdpbmcNCg0KIyMjIDMuMSBJbXB1dGluZyBtaXNzaW5nIGRhdGENCg0KVGhlICQ/JCB2YWx1ZXMgb2YgdGhlIGF0dHJpYnV0ZXMgaW4gdGhlIGBzdHIoKWAgcHJpbnRvdXQgaW5kaWNhdGUgbWlzc2luZyBkYXRhLiBXZSBjYW4gaW1wdXRlIHRoZSBtaXNzaW5nIGRhdGEgdXNpbmcgdGhlIFsqKm1pY2UqKl0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL21pY2UvbWljZS5wZGYpIHBhY2thZ2UuDQo8YnI+DQoNCkZpcnN0IGxldCdzIHJlcGxhY2UgdGhlICQ/JCB3aXRoICROQSQuDQpgYGB7cn0NCm1hbW1vX2RhdGEkQXNzZXNzbWVudFttYW1tb19kYXRhJEFzc2Vzc21lbnQgPT0gIj8iXSA8LSBOQQ0KbWFtbW9fZGF0YSRBc3Nlc3NtZW50IDwtIGZhY3RvcihtYW1tb19kYXRhJEFzc2Vzc21lbnQpDQptYW1tb19kYXRhJEFnZVttYW1tb19kYXRhJEFnZSA9PSAiPyJdIDwtIE5BDQptYW1tb19kYXRhJEFnZSA8LSBhcy5pbnRlZ2VyKG1hbW1vX2RhdGEkQWdlKSAjIEFnZSBpcyBhbiBpbnRlZ2VyDQptYW1tb19kYXRhJFNoYXBlW21hbW1vX2RhdGEkU2hhcGUgPT0gIj8iXSA8LSBOQQ0KbWFtbW9fZGF0YSRTaGFwZSA8LSBmYWN0b3IobWFtbW9fZGF0YSRTaGFwZSkNCm1hbW1vX2RhdGEkTWFyZ2luW21hbW1vX2RhdGEkTWFyZ2luID09ICI/Il0gPC0gTkENCm1hbW1vX2RhdGEkTWFyZ2luIDwtIGZhY3RvcihtYW1tb19kYXRhJE1hcmdpbikNCm1hbW1vX2RhdGEkRGVuc2l0eVttYW1tb19kYXRhJERlbnNpdHkgPT0gIj8iXSA8LSBOQQ0KbWFtbW9fZGF0YSREZW5zaXR5IDwtIGZhY3RvcihtYW1tb19kYXRhJERlbnNpdHkpDQpzdHIobWFtbW9fZGF0YSkNCmBgYA0KPGJyPg0KDQpOb3cgd2UgY2FuIGltcHV0ZSB0aGUgbWlzc2luZyBkYXRhLiBUaGlzIHRha2VzIGEgY291cGxlIG9mIG1pbnV0ZXMgaW4gbXkgY29tcHV0ZXIuDQpgYGB7cn0NCmxpYnJhcnkobWljZSkNCnNldC5zZWVkKDEwMDApDQp2YXJzLmZvci5pbXB1dGF0aW9uIDwtIGMoIkFzc2Vzc21lbnQiLCAiQWdlIiwgIlNoYXBlIiwgIk1hcmdpbiIsICJEZW5zaXR5IikNCmltcHV0ZWQgPC0gY29tcGxldGUobWljZShtYW1tb19kYXRhW3ZhcnMuZm9yLmltcHV0YXRpb25dKSkNCm1hbW1vX2RhdGFbdmFycy5mb3IuaW1wdXRhdGlvbl0gPC0gaW1wdXRlZA0KYGBgDQoNCg0KIyMjIDMuMiBEYXRhIG11bmdpbmcNCg0KJEFzc2Vzc21lbnQkIGlzIHN1cHBvc2VkIHRvIGJlIGFuIG9yZGluYWwgdmFyaWFibGUgdGhhdCBjYW4gdGFrZSB2YWx1ZXMgZnJvbSAkMCQgdG8gJDYkLCBidXQgYHN0cigpYCBzaG93cyA3IGxldmVscy4NCmBgYHtyfQ0KdGFibGUobWFtbW9fZGF0YSRBc3Nlc3NtZW50KQ0KYGBgDQo8YnI+DQoNClRoZXJlIGlzIGEgdmFsdWUgb2YgJDU1JC4gVGhpcyBjb3VsZCBiZSBhIGNhc2Ugb2YgaGl0dGluZyB0aGUgJzUnIGtleSB0d2ljZSBieSBhY2NpZGVudC4NCmBgYHtyfQ0KbWFtbW9fZGF0YSRTZXZlcml0eVttYW1tb19kYXRhJEFzc2Vzc21lbnQgPT0gIjU1Il0NCmBgYA0KPGJyPg0KDQpUaGUgcmVzcG9uc2UgZm9yIHRoYXQgc3BlY2lmaWMgY2FzZSB3YXMgJDEkLCBpLmUuLCB0aGUgbWFzcyB0dXJuZWQgb3V0IHRvIGJlIG1hbGlnbmFudC4gV2UgY2FuIGNoYW5nZSB0aGUgJDU1JCB0byBhICQ1JC4NCmBgYHtyfQ0KbWFtbW9fZGF0YSRBc3Nlc3NtZW50W21hbW1vX2RhdGEkQXNzZXNzbWVudCA9PSAiNTUiXSA8LSAiNSINCiMgUmVzZXQgZmFjdG9yIGxldmVscw0KIyBodHRwczovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy8xMTk1ODI2L2Ryb3AtZmFjdG9yLWxldmVscy1pbi1hLXN1YnNldHRlZC1kYXRhLWZyYW1lDQptYW1tb19kYXRhJEFzc2Vzc21lbnQgPC0gZmFjdG9yKG1hbW1vX2RhdGEkQXNzZXNzbWVudCkgDQoNCnRhYmxlKG1hbW1vX2RhdGEkQXNzZXNzbWVudCkNCmBgYA0KPGJyPg0KDQpUaGUgY2FzZXMgY2xhc3NpZmllZCBhcyAkNiQgYXJlIGNhc2VzIGtub3duIHRvIGJlIG1hbGlnbmFudC4NCjxicj4NCg0KV2UgY2FuIGFsc28gY29udmVydCAkU2V2ZXJpdHkkIHRvIGEgZmFjdG9yLCBpLmUuLCBhbiBvcmRpbmFsIHZhcmlhYmxlLg0KYGBge3J9DQptYW1tb19kYXRhJFNldmVyaXR5IDwtIGZhY3RvcihtYW1tb19kYXRhJFNldmVyaXR5KQ0Kc3VtbWFyeShtYW1tb19kYXRhKQ0KYGBgDQo8YnI+DQoNCiMjIyAzLjMgRGF0YSBleHBsb3JhdGlvbg0KDQpBIHF1aWNrIGxvb2sgYXQgdGhlIGBzdW1tYXJ5KClgIHByaW50b3V0IGFib3ZlIHNob3dzIHRoYXQgdGhlIHZhc3QgbWFqb3JpdHkgb2YgJEFzc2Vzc21lbnQkIHZhbHVlcyBhcmUgZWl0aGVyICQ0JCBvciAkNSQuIEFjY29yZGluZyB0byB0aGUgW0JJLVJBRFNdKGh0dHA6Ly9icmVhc3QtY2FuY2VyLmNhL2JpLXJhZHMvKSBzY2FsZSwgJDQkIGlzIGEgbWFzcyBkZXNjcmliZWQgYXMgYSAic3VzcGljaW91cyBhYm5vcm1hbGl0eSIsIHdoZXJlYXMgYSAkNSQgaXMgb25lICJoaWdobHkgc3VzcGljaW91cyBvZiBtYWxpZ25hbmN5Ii4gV2UgY2FuIHNwbGl0IHRoZSBkYXRhIGludG8gYmVuaWduIGFuZCBtYWxpZ24gYW5kIHNlZSBob3cgdGhlIGFzc2Vzc21lbnRzIGFyZSBkaXN0cmlidXRlZC4NCmBgYHtyfQ0KbWFtbW9fZGF0YV9iZW5pZ24gPC0gbWFtbW9fZGF0YVttYW1tb19kYXRhJFNldmVyaXR5ID09ICIwIixdDQpzdW1tYXJ5KG1hbW1vX2RhdGFfYmVuaWduJEFzc2Vzc21lbnQpDQpgYGANCjxicj4NCg0KJDQxJCBtYXNzZXMgb3V0IG9mICQzNDckIGRlZW1lZCBoaWdobHkgc3VzcGljaW91cyBvZiBiZWluZyBtYWxpZ25hbnQgdHVybmVkIG91dCB0byBiZSBiZW5pZ24sIGFuZCAkNDI3JCBvdXQgb2YgJDU0NyQgY2FzZXMgY2hhcmFjdGVyaXplZCBhcyAic3VzcGljaW91cyIgYWxzbyB0dXJuZWQgb3V0IHRvIGJlIGJlbmlnbi4gQWx0b2dldGhlciwgJDQ2OCQgY2FzZXMgb3V0IG9mICQ4OTQkLCBvciAkNTIuM1wlJCBvZiB0aGUgY2FzZXMgcmVnYXJkZWQgYXMgInN1c3BpY2lvdXMiIG9yICJoaWdobHkgc3VzcGljaW91cyIsIHdlcmUgaW4gZmFjdCBiZW5pZ24uIFRoYXQgaXMgYSBoaWdoIHBlcmNlbnRhZ2Ugb2YgcGVvcGxlIHRoYXQgYXJlIHBvdGVudGlhbGx5IHVuZGVyIGEgbG90IG9mIHN0cmFpbiBmb3IgZGF5cyBvciBldmVuIHdlZWtzLiBUaGVyZSBhcmUgYWxzbyAzIGNhc2VzIGNsYXNzaWZpZWQgYXMgJDYkIHRoYXQgdHVybmVkIG91dCBub3QgdG8gYmUgbWFsaWduYW50LCBhZnRlciBhbGwsIHdoaWNoIGlzIGEgYml0IG15c3RpZnlpbmcsIHNpbmNlIHRob3NlIGFyZSBzdXBwb3NlZCB0byBiZSBjYXNlcyBrbm93biB0byBiZSBtYWxpZ25hbnQsIGFjY29yZGluZyB0byB0aGUgW0JJLVJBRFNdKGh0dHA6Ly9icmVhc3QtY2FuY2VyLmNhL2JpLXJhZHMvKSBzY2hlbWUuDQpgYGB7cn0NCm1hbW1vX2RhdGFfbWFsaWduIDwtIG1hbW1vX2RhdGFbbWFtbW9fZGF0YSRTZXZlcml0eSA9PSAiMSIsXQ0Kc3VtbWFyeShtYW1tb19kYXRhX21hbGlnbiRBc3Nlc3NtZW50KQ0KYGBgDQo8YnI+DQoNCiQ4JCBjYXNlcyBvdXQgb2YgYSB0b3RhbCBvZiAkNTEkIGNsYXNzaWZpZWQgYXMgImJlbmlnbiIgKCQyJCkgb3IgInByb2JhYmx5IGJlbmlnbiIgKCQzJCkgd2VyZSBhY3R1YWxseSBtYWxpZ25hbnQsIG9yIGFib3V0ICQxNS43XCUkLg0KPGJyPg0KDQojIyMgMy40IFNwbGl0dGluZyBpbnRvIHRyYWluaW5nIGFuZCB0ZXN0aW5nIGRhdGFzZXRzDQoNCkxldCdzIHNwbGl0IHRoZSBkYXRhIGludG8gdHJhaW5pbmcgYW5kIHRlc3RpbmcgZGF0YSBzZXRzLg0KYGBge3J9DQpsaWJyYXJ5KGNhVG9vbHMpDQpzZXQuc2VlZCgxMDAwKSAjIHJlcHJvZHVjaWJpbGl0eQ0Kc3BsaXQgPC0gc2FtcGxlLnNwbGl0KG1hbW1vX2RhdGEkU2V2ZXJpdHksIFNwbGl0UmF0aW8gPSAwLjcpDQptYW1tb19kYXRhX3RyYWluIDwtIHN1YnNldChtYW1tb19kYXRhLCBzcGxpdD09VFJVRSkNCm1hbW1vX2RhdGFfdGVzdCA8LSBzdWJzZXQobWFtbW9fZGF0YSwgc3BsaXQ9PUZBTFNFKQ0KYGBgDQo8YnI+DQoNCiMjIDQuMCBEZWNpc2lvbiB0cmVlcw0KDQpMb2FkaW5nIHRoZSByZXF1aXJlZCBsaWJyYXJpZXMNCmBgYHtyfQ0KbGlicmFyeShycGFydCkNCmxpYnJhcnkocnBhcnQucGxvdCkNCmBgYA0KPGJyPg0KDQpOb3cgd2UgYnVpbGQgdGhlIHRyZWUgYW5kIHBsb3QgaXQuIFRoZSBwYXJhbWV0ZXIgKm1pbmJ1Y2tldCogYWxsb3dzIHVzIHRvIHNlbGVjdCBob3cgdGhlIG1pbmltdW0gbnVtYmVyIG9mIHRoYXQgYXJlIHBsYWNlZCBpbiBvbmUgb2YgdGhlIHRyZWUncyAibGVhdmVzIiBvciAiYnVja2V0cyIuDQpgYGB7cn0NCiMgQnVpbGRpbmcgYSB0cmVlIHdpdGggYSBtaW5pbXVtIG9mIDEwIG9ic2VydmF0aW9ucyBvbiBlYWNoIGxlYWYNCm1hbW1vZ190cmVlIDwtIHJwYXJ0KFNldmVyaXR5IH4gLiwgZGF0YSA9IG1hbW1vX2RhdGFfdHJhaW4sIGNvbnRyb2w9cnBhcnQuY29udHJvbChtaW5idWNrZXQ9MTApKQ0KcHJwKG1hbW1vZ190cmVlKQ0KYGBgDQo8YnI+DQoNClRoZSB0cmVlIGlzIG5vdCBoYXJkIHRvIGludGVycHJldC4gSWYgJEFzc2Vzc21lbnQkIGlzICQ1JCBvciAkMCQgb3IgJDYkLCB0aGUgdHJlZSBwcmVkaWN0cyAkMSQsIG9yIG1hbGlnbmFudC4gT3RoZXJ3aXNlLCB0aGUgdHJlZSBsb29rcyBhdCAkU2hhcGUkIGFuZCAkQWdlJCB0byBwcmVkaWN0IGFuIG91dGNvbWUuIEhhdmluZyBidWlsdCB0aGUgdHJlZSwgd2UgY2FuIGNoZWNrIGl0cyBhY2N1cmFjeSBvbiB0aGUgdHJhaW5pbmcgc2V0IGl0c2VsZi4NCmBgYHtyfQ0KIyBHZW5lcmF0ZSBwcmVkaWN0aW9ucyBvbiB0cmFpbmluZyBzZXQNClByZWRpY3RDQVJUX3RyYWluID0gcHJlZGljdChtYW1tb2dfdHJlZSwgdHlwZSA9ICJjbGFzcyIpDQojIENvbmZ1c2lvbiBtYXRyaXggb2YgdHJhaW5pbmcgc2V0DQpjb25mX21hdHJpeF90cmFpbiA8LSB0YWJsZShtYW1tb19kYXRhX3RyYWluJFNldmVyaXR5LCBQcmVkaWN0Q0FSVF90cmFpbikNCmNvbmZfbWF0cml4X3RyYWluDQpgYGANCjxicj4NCg0KVGhlIHJvd3MgYXJlIHRoZSBncm91bmQgdHJ1dGggd2hlcmVhcyB0aGUgY29sdW1ucyBhcmUgdGhlIHByZWRpY3Rpb25zIGdlbmVyYXRlZCBieSB0aGUgdHJlZS4gRm9yIGV4YW1wbGUsIG91dCBvZiB0aG9zZSB0cmFpbmluZyBzZXQgb2JzZXJ2YXRpb25zIHRoYXQgd2VyZSBiZW5pZ24sIHRoZSB0cmVlIGNvcnJlY3RseSBsYWJlbHMgJDMxOCQgb2YgdGhlbSBhcyBzdWNoIGFuZCAkNDMkIG9mIHRoZW0gaW5jb3JyZWN0bHkgYXMgbWFsaWduLiBXZSBjYW4gY29tcHV0ZSB0aGUgYWNjdXJhY3kgYXMgdGhlIHN1bSBvZiB0aGUgdHJ1ZSBuZWdhdGl2ZXMgYW5kIHRydWUgcG9zaXRpdmVzIGFuZCBkaXZpZGluZyBieSB0aGUgdG90YWwgbnVtYmVyIG9mIG9ic2VydmF0aW9ucy4NCg0KJCQNCkFjY3VyYWN5ID0gXGZyYWN7VHJ1ZVwgUG9zaXRpdmVzXCArXCBUcnVlXCBOZWdhdGl2ZXN9e1RydWVcIFBvc2l0aXZlc1wgK1wgVHJ1ZVwgTmVnYXRpdmVzXCArXCBGYWxzZVwgUG9zaXRpdmVzXCArXCBGYWxzZVwgTmVnYXRpdmVzfSA9IFxmcmFje051bWJlclwgb2ZcIGNhc2VzXCBsYWJlbGxlZFwgY29ycmVjdGx5fXtUb3RhbFwgbnVtYmVyXCBvZlwgY2FzZXN9DQokJA0KDQpgYGB7cn0NCnN1bShkaWFnKGNvbmZfbWF0cml4X3RyYWluKSkgLyBzdW0oY29uZl9tYXRyaXhfdHJhaW4pDQpgYGANCjxicj4NCg0KIyMjIDQuMSBNYWtlIHByZWRpY3Rpb25zIG9uIHRoZSB0ZXN0IHNldCB1c2luZyB0aGUgQ0FSVCBtb2RlbA0KDQpXZSBjYW4gdGhlbiBtYWtlIHByZWRpY3Rpb25zIHVzaW5nIHRoZSB0cmVlIG9uIHRoZSB0ZXN0IHNldC4NCmBgYHtyfQ0KIyBHZW5lcmF0ZSBwcmVkaWN0aW9ucyBvbiB0ZXN0IHNldA0KUHJlZGljdENBUlRfdGVzdCA9IHByZWRpY3QobWFtbW9nX3RyZWUsIG5ld2RhdGEgPSBtYW1tb19kYXRhX3Rlc3QsIHR5cGUgPSAiY2xhc3MiKQ0KY29uZl9tYXRyaXhfdGVzdCA8LSB0YWJsZShtYW1tb19kYXRhX3Rlc3QkU2V2ZXJpdHksIFByZWRpY3RDQVJUX3Rlc3QpDQpjb25mX21hdHJpeF90ZXN0DQpgYGANCjxicj4NCg0KQWNjdXJhY3kgb24gdGhlIHRlc3Qgc2V0Og0KYGBge3J9DQpzdW0oZGlhZyhjb25mX21hdHJpeF90ZXN0KSkgLyBzdW0oY29uZl9tYXRyaXhfdGVzdCkNCmBgYA0KPGJyPg0KDQpUaGUgYWNjdXJhY3kgb24gdGhlIHRlc3Qgc2V0IGlzIGEgbGl0dGxlIHdvcnNlLg0KDQojIyMgNC4yIENyb3NzLXZhbGlkYXRpb24NCg0KV2UgY2FuIHVzZSBjcm9zcy12YWxpZGF0aW9uIHRvIG9idGFpbiB0aGUgdHJlZSB0aGF0IG1heGltaXplcyBhY2N1cmFjeS4gVGhlcmUgaXMgYSBwYXJhbWV0ZXIgY2FsbGVkICpjcCosIG9yIGNvbXBsZXhpdHkgcGFyYW1ldGVyLCB0aGF0IGFsbG93cyB1cywgaW5kaXJlY3RseSwgdG8gc3BlY2lmeSBob3cgbWFueSBvYnNlcnZhdGlvbnMgd2lsbCBiZSBwbGFjZWQgb24gZWFjaCBsZWFmLiBXZSBjYW4gZG8gMTAtZm9sZCBjcm9zcy12YWxpZGF0aW9uIGZvciBlYWNoICpjcCogdmFsdWUsIGNvbXB1dGUgdGhlIGF2ZXJhZ2UgYWNjdXJhY3kgb2YgZWFjaCAqY3AqIHZhbHVlLCBhbmQgdGhlbiB0YWtlIHRoZSBiZXN0ICpjcCogdmFsdWUgYW5kIGJ1aWxkIHRoZSBmaW5hbCB0cmVlIHdpdGggaXQuDQo8YnI+DQoNCkxldCdzIGxvYWQgdGhlIGxpYnJhcmllcyB3ZSB3aWxsIG5lZWQgZm9yIHRoZSBjcm9zcy12YWxpZGF0aW9uOg0KYGBge3J9DQpsaWJyYXJ5KGNhcmV0KQ0KbGlicmFyeShlMTA3MSkNCmBgYA0KPGJyPg0KDQpOb3cgZGVmaW5lIHRoZSBwYXJhbWV0ZXJzIG9mIHRoZSBjcm9zcy12YWxpZGF0aW9uLiBXZSB3aWxsIGRvIDEwLWZvbGQgQ1YgZm9yIGVhY2ggb2YgNTAgdmFsdWVzIG9mICpjcCosIGZyb20gJDAuMDEkIHRvICQwLjUwJC4NCmBgYHtyfQ0KIyBTZXR0aW5nIGNyb3NzLXZhbGlkYXRpb24gdG8gYmUgMTAtZm9sZA0KZml0Q29udHJvbCA9IHRyYWluQ29udHJvbCggbWV0aG9kID0gImN2IiwgbnVtYmVyID0gMTAgKQ0KIyBTZXR0aW5nIGNwIHRvIC4wMSwgLjAyLCAuLi4sIDAuNQ0KY2FydEdyaWQgPSBleHBhbmQuZ3JpZCggLmNwID0gKDE6NTApKjAuMDEpIA0KYGBgDQo8YnI+DQoNClBlcmZvcm0gdGhlIGNyb3NzLXZhbGlkYXRpb24gdG8gZGV0ZXJtaW5lIHRoZSBiZXN0ICpjcCogcGFyYW1ldGVyLiBUaGUgYHRyYWluKClgIGZ1bmN0aW9uIGRvZXMgMTAtZm9sZCBDViBmb3IgZWFjaCAqY3AqIHZhbHVlLCBzbyBpdCBidWlsZHMgMTAgdHJlZXMgZm9yIGVhY2ggKmNwKiBhbmQgY29tcHV0ZXMgdGhlIGF2ZXJhZ2UgYWNjdXJhY3kgYXNzb2NpYXRlZCB3aXRoIHRoYXQgKmNwKi4gU2luY2UgaXQgZG9lcyB0aGlzIGZvciBlYWNoIHZhbHVlIG9mICpjcCosIGB0cmFpbigpYCBidWlsZHMgNTAwIHRyZWVzLiBJdCB0YWtlcyBhIGxpdHRsZSB3aGlsZS4NCmBgYHtyfQ0Kc2V0LnNlZWQoMTAwKQ0KdHJhaW4oU2V2ZXJpdHkgfiAuLCBkYXRhID0gbWFtbW9fZGF0YV90cmFpbiwgbWV0aG9kID0gInJwYXJ0IiwgdHJDb250cm9sID0gZml0Q29udHJvbCwgdHVuZUdyaWQgPSBjYXJ0R3JpZCkNCmBgYA0KPGJyPg0KDQpUaGUgYHRyYWluKClgIGZ1bmN0aW9uIGZyb20gdGhlICoqY2FyZXQqKiBwYWNrYWdlIHNlbGVjdHMgdGhlIGJlc3QgdmFsdWUgb2YgKmNwKjogJDAuMDIkLiBXZSBjYW4gdGhlbiB1c2UgdGhpcyB2YWx1ZSB0byBidWlsZCB0aGUgZmluYWwgdHJlZToNCmBgYHtyfQ0KbWFtbW9nX3RyZWVfY3YgPC0gcnBhcnQoU2V2ZXJpdHkgfiAuLCBkYXRhID0gbWFtbW9fZGF0YV90cmFpbiwgY29udHJvbD1ycGFydC5jb250cm9sKGNwPTAuMDIpKQ0KcHJwKG1hbW1vZ190cmVlX2N2KQ0KYGBgDQo8YnI+DQoNCldlIGdldCB0aGUgc2FtZSB0cmVlIGFzIGJlZm9yZS4gVGhpcyB0cmVlIGhhcyBhYm91dCA4MCUgYWNjdXJhY3kgb24gdGhlIHRlc3Qgc2V0LiBGb3IgdGhlIGNhc2VzIGNsYXNzaWZpZWQgYXMgJDUkLCBpdCBhdXRvbWF0aWNhbGx5IGNsYXNzaWZpZXMgdGhlbSBhcyBtYWxpZ25hbnQsIGV2ZW4gdGhvdWdoIHdlIHNhdyBlYXJsaWVyIHRoYXQgcXVpdGUgYSBmZXcgb2YgdGhvc2UgaW5kZWVkIHdlcmUgbm90LiBUaGF0J3MgYSBiaXQgZGlzYXBwb2ludGluZy4NCjxicj4NCg0KIyMjIDQuMyBCdWlsZGluZyBhIHRyZWUgdXNpbmcgb25seSB0aGUgcGh5c2ljYWwgYXR0cmlidXRlcw0KDQpXaGF0IGlmIHdlIHdlcmUgdG8gYnVpbGQgYSB0cmVlIHVzaW5nIGp1c3QgdGhlIHBoeXNpY2FsIGF0dHJpYnV0ZXMgb2YgdGhlIG1hc3M/DQpgYGB7cn0NCnNldC5zZWVkKDEwMCkNCnRyYWluKFNldmVyaXR5IH4gU2hhcGUgKyBNYXJnaW4gKyBEZW5zaXR5LCBkYXRhID0gbWFtbW9fZGF0YV90cmFpbiwgbWV0aG9kID0gInJwYXJ0IiwgdHJDb250cm9sID0gZml0Q29udHJvbCwgdHVuZUdyaWQgPSBjYXJ0R3JpZCkNCmBgYA0KPGJyPg0KDQpCdWlsZGluZyB0aGUgdHJlZSB3aXRoIHRoZSAqY3AqIHZhbHVlIHBpY2tlZCBieSBgdHJhaW4oKWANCmBgYHtyfQ0KbWFtbW9nX3RyZWVfY3ZfcGh5c2ljYWwgPC0gcnBhcnQoU2V2ZXJpdHkgfiBTaGFwZSArIE1hcmdpbiArIERlbnNpdHksIGRhdGEgPSBtYW1tb19kYXRhX3RyYWluLCBjb250cm9sPXJwYXJ0LmNvbnRyb2woY3A9MC4wMSkpDQpwcnAobWFtbW9nX3RyZWVfY3ZfcGh5c2ljYWwpDQpgYGANCjxicj4NCg0KQ29tcHV0aW5nIHRoZSBhY2N1cmFjeSBvZiB0aGUgdHJlZSBvbiB0aGUgdGVzdCBzZXQNCmBgYHtyfQ0KUHJlZGljdENBUlRfdGVzdF9DVl9waHlzaWNhbCA9IHByZWRpY3QobWFtbW9nX3RyZWVfY3ZfcGh5c2ljYWwsIG5ld2RhdGEgPSBtYW1tb19kYXRhX3Rlc3QsIHR5cGUgPSAiY2xhc3MiKQ0KY29uZl9tYXRyaXhfdGVzdF9DVl9waHlzaWNhbCA8LSB0YWJsZShtYW1tb19kYXRhX3Rlc3QkU2V2ZXJpdHksIFByZWRpY3RDQVJUX3Rlc3RfQ1ZfcGh5c2ljYWwpDQpjb25mX21hdHJpeF90ZXN0X0NWX3BoeXNpY2FsDQpgYGANCjxicj4NCg0KYGBge3J9DQpzdW0oZGlhZyhjb25mX21hdHJpeF90ZXN0X0NWX3BoeXNpY2FsKSkgLyBzdW0oY29uZl9tYXRyaXhfdGVzdF9DVl9waHlzaWNhbCkNCmBgYA0KPGJyPg0KDQpUaGUgYWNjdXJhY3kgb2YgdGhpcyB0cmVlIGlzIGFsbW9zdCB0aGUgc2FtZSBhcyB0aGF0IG9mIHRoZSB0cmVlIHRoYXQgaW5jbHVkZXMgdGhlIEJJUkFEUyBhc3Nlc3NtZW50Lg0KPGJyPg0KDQpPdmVyYWxsLCBJIHRoaW5rIHRoaXMgZGF0YXNldCB3b3VsZCBoYXZlIGJlZW4gbW9yZSB1c2VmdWwgaWYgaXQgaGFkIG1vcmUgcHJlZGljdG9ycyBhdmFpbGFibGUuIE1vcmUgc3BlY2lmaWNhbGx5LCBtb3JlIHBoeXNpY2FsIGF0dHJpYnV0ZXMgZnJvbSB0aGUgbWFzcyBjb3VsZCBwZXJoYXBzIGVuYWJsZSB1cyB0byBzZWUgaWYgdGhlIEJJLVJBRFMgYXNzZXNzbWVudCBjb3VsZCBiZSBpbXByb3ZlZCB1cG9uLiBXZSBjb3VsZCBhbHNvIGJ1aWxkIGEgdHJlZSB0byB0cnkgdG8gcHJlZGljdCB0aGUgJEFzc2Vzc21lbnQkIG9mIHRoZSByYWRpb2xvZ2lzdCwgd2hvIHVzZXMgW21hbnldKGh0dHA6Ly9icmVhc3QtY2FuY2VyLmNhL2JpLXJhZHMvKSBtb3JlIGZhY3RvcnMgdGhhbiBzaW1wbHkgc2hhcGUsIG1hcmdpbiwgYW5kIGRlbnNpdHkgdG8gZGV0ZXJtaW5lIGNsYXNzaWZpY2F0aW9uLg0KDQojIyBSZWZlcmVuY2VzDQoNCjEuIEJlcnRzaW1hcywgRC4sIE8nSGFpciwgQS4gKioqVGhlIEFuYWx5dGljcyBFZGdlKioqLiBTcHJpbmcgMjAxNC4gW2VkWC5vcmddKHd3dy5lZFgub3JnKS4NCg==